Tell me more ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

I've put together this gist with an idea about testing WordPress themes and plugins with Mockery and a WordPress facade object. I've love some feedback on the approach, please.

WordPressFacade

<?php 
/**
 * I hate unit testing plugins and themes in WordPress. The standard WP unit testing 
 * libraries require bootstrapping the entire framework, make actual calls to the database,
 * and then wipe it after each one. 
 * 
 * It's slow. Painfully slow. And, it takes forever to get setup.
 * 
 * So, I came up with this idea. I really have no idea if it's a good idea or not, but I'm putting
 * it out there anyway. 
 * 
 * Instead of plugins and themes making direct calls the WordPress objects and functions, I propose
 * we use a WordPress facade object instead. This facade (and whatever functions are needed) can then
 * be easily mocked with Mockery to return testable results without all the extra work of bootstrapping
 * WordPress in its entirety.
 * 
 */


/**
* Acts as a master facade class for WordPress to allow me to isolate and test
* functionality without bootstrapping all of WordPress.
*
* @category Category
* @package  Package
* @author   Kevin Perrine <kperrine@gmail.com>
* @license  MIT
*/
class WordPressFacade
{
    /**
     * Magic __call method that creates a facade for all global wordpress functions.
     * Which actually ends up being a facade for all global functions including those
     * in plugins.
     *
     * @param string $method    The WordPress function you want to call.
     * @param mixed  $arguments The arguments passed to the function
     *
     * @access public
     *
     * @return mixed The returns value from the WP function
     */
    public function __call($method, $arguments)
    {
        if (function_exists($method)) {
            return call_user_func_array($method, $arguments);
        }

        throw new Exception(sprintf('The function, %1$s, does not exist.', $function));
    }

    /**
     * Facade method for returning the current $post object.
     *
     * @access public
     *
     * @return Object The WordPress global $post object
     */
    public function post()
    {
        global $post;

        return $post;
    }

    /**
     * Returns the global $wpdb object
     *
     * @access public
     *
     * @return Wpdb WordPress's global $wpdb object
     */
    public function wpdb()
    {
        global $wpdb;

        return $wpdb;
    }

    /**
     * Facade method for creating new WP_Query objects
     *
     * @param mixed Either a string or array of arguments passed to WP_Query
     *
     * @access public
     *
     * @return WP_Query
     */
    public function newQuery($args)
    {
        return new WP_Query($args);
    }

    /**
     * Returns the current global WP_Query object
     *
     * @access public
     *
     * @return WP_Query
     */
    public function wpQuery()
    {
        global $wp_query;

        return $wp_query;
    }

    /**
     * Returns WordPress's global $wp object.
     *
     * @access public
     *
     * @return WP Object
     */
    public function wp()
    {
        global $wp;

        return $wp;
    }
}

ThemeClass

<?php //ThemeClass.php
/**
 * This is a straightforward example of what a ThemeClass may look like. It contains all the
 * expected initialization, and wp_enqueue_script calls.
 */
class ThemeClass
{
    protected $wp;

    protected $themeDirectory;

    public function __construct(WordPressFacade $facade = null)
    {
        if ($facade) {
            $this->setFacade($facade);
            $this->setThemeDirectory();
        }
    }

    public function setFacade(WordPressFacade $facade)
    {
        $this->wp = $facade;
    }

    public function setThemeDirectory()
    {
        $this->themeDirectory = $this->wp->get_bloginfo('template_directory') . '/';
    }

    public function initialize()
    {
        $this->wp->add_action('wp_enqueue_scripts', array($this, 'enqueueScripts'));
        $this->wp->add_action('wp_enqueue_scripts', array($this, 'enqueueStyles'));
        $this->wp->add_action('after_setup_theme', array( $this, 'initialize' ));
        $this->wp->add_action('widgets_init', array($this, 'registerSidebars'));

        /**
        * Makes Twenty Twelve available for translation.
        *
        * Translations can be added to the /languages/ directory.
        * If you're building a theme based on Twenty Twelve, use a find and replace
        * to change 'twentytwelve' to the name of your theme in all the template files.
        */
        $this->wp->load_theme_textdomain('themeClass', $this->assetUrl('/languages'));

        // This theme styles the visual editor with editor-style.css to match the theme style.
        $this->wp->add_editor_style();

        // Adds RSS feed links to <head> for posts and comments.
        $this->wp->add_theme_support('automatic-feed-links');

        // This theme uses wp_nav_menu() in one location.
        $this->wp->register_nav_menu('primary', $this->wp->__('Primary Menu', 'themeClass'));

        // This theme uses a custom image size for featured images, displayed on "standard" posts.
        $this->wp->add_theme_support('post-thumbnails');
        $this->wp->set_post_thumbnail_size(624, 9999); // Unlimited height, soft crop
    }

    public function registerSidebars()
    {
        $this->wp->register_sidebar(
            array(
                'before_widget' => '<section>',
                'after_widget' => '</section>',
                'before_title' => '<h2 class="widgettitle">',
                'after_title' => '</h2>',
            )
        );
    }

    public function enqueueScripts()
    {
        if (WP_DEBUG) {
            $this->loadDevelopmentScripts();
        } else {
            $this->loadProductionScripts();
        }
    }

    protected function loadDevelopmentScripts()
    {
        if ($this->wp->is_admin()) {
            return;
        }

        $this->deregisterScripts();

        $this->wp->wp_enqueue_script(
            'modernizer',
            $this->assetUrl('js/vendor/modernizr.js'),
            '',
            '2.6.2',
            false
        );

        $this->wp->wp_enqueue_script(
            'jquery',
            $this->assetUrl('js/vendor/jquery.js'),
            '',
            '1.9.0',
            true
        );

        $this->wp->wp_enqueue_script(
            'underscore',
            $this->assetUrl('js/vendor/underscore.js'),
            '',
            '1.4.3',
            true
        );

        $this->wp->wp_enqueue_script(
            'backbone',
            $this->assetUrl('js/vendor/backbone.js'),
            'underscore',
            '0.9.9',
            true
        );

        $this->wp->wp_enqueue_script(
            'plugins',
            $this->assetUrl('js/plugins.js'),
            '',
            '1.0',
            true
        );

        $this->wp->wp_enqueue_script(
            'main',
            $this->assetUrl('js/main.js'),
            'jquery',
            '1.0',
            true
        );
    }

    protected function deregisterScripts()
    {
        $this->wp->wp_deregister_script('modernizr');
        $this->wp->wp_deregister_script('jquery');
        $this->wp->wp_deregister_script('backbone');
        $this->wp->wp_deregister_script('underscore');
    }

    protected function loadProductionScripts()
    {
        if ($this->wp->is_admin()) {
            return;
        }

        $this->deregisterScripts();

        $this->wp->wp_enqueue_script(
            'modernizer',
            $this->assetUrl('js/vendor/modernizr.min.js'),
            '',
            '2.6.2',
            false
        );

        $this->wp->wp_enqueue_script(
            'scripts',
            $this->assetUrl('js/scripts.min.js'),
            'google-jq',
            '1.0',
            true
        );
    }


    public function enqueueStyles()
    {
        $this->wp->wp_enqueue_style(
            'themeClass',
            $this->assetUrl('css/themeClass.css')
        );
    }

    public function assetUrl($filePath = '')
    {
        return $this->themeDirectory . $filePath;
    }
}

ThemeClassTest

<?php ThemeClassTest.php
/**
 * The test file uses Mockery to create a mock instance of the WordPress facade and then
 * fill it with the functions and return values needed to test this particular function.
 * This allows easy testing of functions that may only use a few WordPress functions,
 * but give complex results that need to be tested.
 */
use \Mockery as m;

class ThemeTest extends PHPUnit_Framework_TestCase
{
    protected $themeClass;

    public function setUp()
    {
        $this->themeClass = new ThemeClass();
    }

    public function tearDown()
    {
        m::close();
    }

    public function testContructor()
    {
        $wp = m::mock('WordPressFacade');

        $wp->shouldReceive('get_bloginfo')->andReturn('/wp/wp-content/themes/themeDirectory');

        $this->themeClass = new ThemeClass($wp);

        $this->assertEquals('/wp/wp-content/themes/themeDirectory/', $this->themeClass->assetUrl());
    }

    public function testInitialize()
    {
        $wp = m::mock('WordPressFacade');

        $wp->shouldReceive('add_action')->times(4);
        $wp->shouldReceive('load_theme_textdomain');
        $wp->shouldReceive('add_editor_style');
        $wp->shouldReceive('add_theme_support');
        $wp->shouldReceive('register_nav_menu');
        $wp->shouldReceive('add_theme_support');
        $wp->shouldReceive('set_post_thumbnail_size');
        $wp->shouldReceive('__');

        $this->themeClass->setFacade($wp);
        $this->themeClass->initialize();

        /**
         * Add asserts here to verify outcome.
         */
    }

    public function testEnqueueStyles()
    {
        $wp = m::mock('WordPressFacade');

        $wp->shouldReceive('wp_enqueue_style')->with('themeClass', 'http://local.newacl.com/wp/wp-content/themes/themeClass/css/themeClass.css');
        $wp->shouldReceive('get_bloginfo')->with('template_directory')->andReturn('http://local.newacl.com/wp/wp-content/themes/themeClass');
        $wp->shouldReceive('wp_style_is')->with('themeClass', 'queue')->andReturn(true);

        $this->themeClass->setFacade($wp);
        $this->themeClass->setThemeDirectory();

        $this->themeClass->enqueueStyles();
        $this->assertTrue($wp->wp_style_is('themeClass', 'queue'));
    }

    public function testEnqueueScripts()
    {
        $wp = m::mock('WordPressFacade');

        $wp->shouldReceive('wp_enqueue_script')->times(6);
        $wp->shouldReceive('get_bloginfo')->with('template_directory')->andReturn('http://local.newacl.com/wp/wp-content/themes/themeClass');
        $wp->shouldReceive('wp_script_is')->with('modernizr', 'queue')->andReturn(true);
        $wp->shouldReceive('is_admin')->andReturn(false);
        $wp->shouldReceive('wp_deregister_script')->times(4);

        $this->themeClass->setFacade($wp);
        $this->themeClass->setThemeDirectory();

        $this->themeClass->enqueueScripts();

        $this->assertTrue($wp->wp_script_is('modernizr', 'queue'));
        $this->assertEquals('http://local.newacl.com/wp/wp-content/themes/themeClass/js/vendor/modernizr-2.6.2.min.js', $this->themeClass->assetUrl('js/vendor/modernizr-2.6.2.min.js'));
    }

    public function testRegisterSidebars()
    {
        $wp = m::mock('WordPressFacade');

        $wp->shouldReceive('get_bloginfo');
        $wp->shouldReceive('register_sidebar')->with(array(
            'before_widget' => '<section>',
            'after_widget' => '</section>',
            'before_title' => '<h2 class="widgettitle">',
            'after_title' => '</h2>',
        ));

        $this->themeClass->setFacade($wp);
        $this->themeClass->setThemeDirectory();

        $this->themeClass->registerSidebars();
    }
}
share|improve this question
@JeffVanzella, I've added the code. – Kevin Perrine Jan 17 at 20:28

Know someone who can answer? Share a link to this question via email, Google+, Twitter, or Facebook.

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Browse other questions tagged or ask your own question.