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();
}
}