Skip to content

Instantly share code, notes, and snippets.

@tzkmx
Created July 25, 2016 18:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tzkmx/0ee6d862fde8abcbcde83e1b81333ebb to your computer and use it in GitHub Desktop.
Save tzkmx/0ee6d862fde8abcbcde83e1b81333ebb to your computer and use it in GitHub Desktop.
Towards a more testable WordPress. The main idea is simple. Instead of piling everything into a theme's functions.php file, create a class to hold all the theme's functions. Then, using dependency injection, add the WordPress facade and inside the ThemeClass make all calls to the facade instead of the global WP functions. This allow us to use Mo…
<?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;
}
}
<?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();
}
}
<?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.', $method));
}
/**
* 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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment