Test in isolation
When developing a plugin, the best way to test it is without loading the WordPress environment.
If you write code that can be easily tested without WordPress, your code becomes better.
Every component that is unit tested, should be tested in isolation: when you test a class, you only have to test that specific class, assuming all other code is working perfectly.
This is the reason why unit tests are called "unit".
As an additional benefit, without loading core, your test will run much faster.
Avoid hooks in constructor
A tip I can give you is to avoid putting hooks in constructors. That\'s one of the things that will make your code testable in isolation.
Let\'s see test code in OP:
class CustomPostTypes extends WP_UnitTestCase {
function test_custom_post_type_creation() {
$this->assertTrue( post_type_exists( \'foo\' ) );
}
}
And let\'s assume this test fails. Who is the culprit?
- the hook was not added at all or not properly?
- the method that register the post type was not called at all or with wrong arguments?
- there is a bug in WordPress?
How it can be improved?
Let\'s assume your class code is:
class RegisterCustomPostType {
function init() {
add_action( \'init\', array( $this, \'register_post_type\' ) );
}
public function register_post_type() {
register_post_type( \'foo\' );
}
}
(Note: I will refer to this version of the class for the rest of the answer)
The way I wrote this class allows you to create instances of the class without calling add_action
.
In the class above there are 2 things to be tested:
- the method
init
actually calls add_action
passing to it proper arguments
- the method
register_post_type
actually calls register_post_type
function
I didn\'t say that you have to check if post type exists: if you add the proper action and if you call register_post_type
, the custom post type must exist: if it doesn\'t exists it\'s a WordPress problem.
Remember: when you test your plugin you have to test your code, not WordPress code. In your tests you have to assume that WordPress (just like any other external library you use) works well. That\'s the meaning of unit test.
But... in practice?
If WordPress is not loaded, if you try to call class methods above, you get a fatal error, so you need to mock the functions.
The "manual" method
Sure you can write your mocking library or "manually" mock every method. It\'s possible. I\'ll tell you how to do that, but then I\'ll show you an easier method.
If WordPress is not loaded while tests are running, it means you can redefine its functions, e.g. add_action
or register_post_type
.
Let\'s assume you have a file, loaded from your bootstrap file, where you have:
function add_action() {
global $counter;
if ( ! isset($counter[\'add_action\']) ) {
$counter[\'add_action\'] = array();
}
$counter[\'add_action\'][] = func_get_args();
}
function register_post_type() {
global $counter;
if ( ! isset($counter[\'register_post_type\']) ) {
$counter[\'register_post_type\'] = array();
}
$counter[\'register_post_type\'][] = func_get_args();
}
I re-wrote the functions to simply add an element to a global array every time they are called.
Now you should create (if you don\'t have one already) your own base test case class extending PHPUnit_Framework_TestCase
: that allows you to easily configure your tests.
It can be something like:
class Custom_TestCase extends \\PHPUnit_Framework_TestCase {
public function setUp() {
$GLOBALS[\'counter\'] = array();
}
}
In this way, before every test, the global counter is reset.
And now your test code (I refer to the rewritten class I posted above):
class CustomPostTypes extends Custom_TestCase {
function test_init() {
global $counter;
$r = new RegisterCustomPostType;
$r->init();
$this->assertSame(
$counter[\'add_action\'][0],
array( \'init\', array( $r, \'register_post_type\' ) )
);
}
function test_register_post_type() {
global $counter;
$r = new RegisterCustomPostType;
$r->register_post_type();
$this->assertSame( $counter[\'register_post_type\'][0], array( \'foo\' ) );
}
}
You should note:
- I was able to call the two methods separately and WordPress is not loaded at all. This way if one test fails, I know exactly who the culprit is.
- As I said, here I test that the classes call WP functions with expected arguments. There is no need to test if CPT really exists. If you are testing the existence of CPT, then you are testing WordPress behavior, not your plugin behavior...
Nice.. but it\'s a PITA!
Yes, if you have to manually mock all the WordPress functions, it\'s really a pain.
Some general advice I can give is to use as few WP functions as possible: you don\'t have to rewrite WordPress, but abstract WP functions you use in custom classes, so that they can be mocked and easily tested.
E.g. regarding example above, you can write a class that registers post types, calling register_post_type
on \'init\' with given arguments.
With this abstraction you still need to test that class, but in other places of your code that register post types you can make use of that class, mocking it in tests (so assuming it works).
The awesome thing is, if you write a class that abstracts CPT registration, you can create a separate repository for it, and thanks to modern tools like Composer embed it in all the projects where you need it: test once, use everywhere. And if you ever find a bug in it, you can fix it in one place and with a simple composer update
all the projects where it is used are fixed too.
For the second time: to write code that is testable in isolation means to write better code.
But sooner or later I need to use WP functions somewhere...
Of course. You never should act in parallel to core, it makes no sense. You can write classes that wraps WP functions, but those classes need to be tested too. The "manual" method described above may be used for very simple tasks, but when a class contains a lot of WP functions it can be a pain.
Luckily, over there there are good people that write good things. 10up, one of the biggest WP agencies, maintains a very great library for people that want to test plugins the right way. It is WP_Mock
.
It allows you to mock WP functions an hooks. Assuming you have loaded in your tests (see repo readme) the same test I wrote above becomes:
class CustomPostTypes extends Custom_TestCase {
function test_init() {
$r = new RegisterCustomPostType;
// tests that the action was added with given arguments
\\WP_Mock::expectActionAdded( \'init\', array( $r, \'register_post_type\' ) );
$r->init();
}
function test_register_post_type() {
// tests that the function was called with given arguments and run once
\\WP_Mock::wpFunction( \'register_post_type\', array(
\'times\' => 1,
\'args\' => array( \'foo\' ),
) );
$r = new RegisterCustomPostType;
$r->register_post_type();
}
}
Simple, isn\'t it? This answer is not a tutorial for WP_Mock
, so read the repo readme for more info, but the example above should be pretty clear, I think.
Moreover, you don\'t need to write any mocked add_action
or register_post_type
by yourself, or maintain any global variables.
And WP classes?
WP has some classes too, and if WordPress is not loaded when you run tests, you need to mock them.
That\'s much easier than mocking functions, PHPUnit has an embedded system to mock objects, but here I want to suggest Mockery to you. It\'s a very powerful library and very easy to use. Moreover, it\'s a dependency of WP_Mock
, so if you have it you have Mockery too.
But what about WP_UnitTestCase
?
The WordPress test suite was created to test WordPress core, and if you want to contribute to core it is pivotal, but using it for plugins only makes you test not in isolation.
Put your eyes over WP world: there are a lot of modern PHP frameworks and CMS out there and none of them suggests testing plugin/modules/extensions (or whatever they are called) using framework code.
If you miss factories, a useful feature of the suite, you have to know that there are awesome things over there.
Gotchas and downsides
There is a case when the workflow I suggested here lacks: custom database testing.
In fact, if you use standard WordPress tables and functions to write there (at the lowest level $wpdb
methods) you never need to actually write data or test if data is actually in database, just be sure that proper methods are called with proper arguments.
However, you can write plugins with custom tables and functions that build queries to write there, and test if those queries work it\'s your responsibility.
In those cases WordPress test suite can helps you a lot, and loading WordPress may be needed in some cases to run functions like dbDelta
.
(There is no need to say to use a different db for tests, isn\'t it?)
Luckily PHPUnit allows you to organize your tests in "suites" that can be run separately, so you can write a suite for custom database tests where you load WordPress environment (or part of it) leaving all the rest of your tests WordPress-free.
Only be sure to write classes that abstract as many database operations as possible, in a way that all the other plugin classes make use of them, so that using mocks you can properly test the majority of classes without dealing with database.
For third time, writing code easily testable in isolation means writing better code.