Steve Grunwell is a Senior Software Engineer at Liquid Web who works on Managed WordPress platforms, specializing in WP and web app development. He says WordPress is a tightly-coupled system with a history of ideas, decisions and technical shifts that can mean consequences for even simpler tasks. However, you can ensure software is released regularly with low regression risk with automated testing.
Building WordPress plugins with tests can seem challenging, however there are tools to set up a test harness within an existing codebase with ease. In his WCUS talk, Steve talked about the fundamentals of automated testing, particularly in regards to WordPress. Plus, how to start testing plugins and themes using features from PHPUnit and the WordPress core testing framework. In order to finally build and release quality software.
About Automated Testing
Achieving continuous integration and delivery is the holy grail. We can start automated the entire process from writing code to production. Automated testing plays a vital part in letting us reduce time and chance of human error. It is easily reproducible and a gateway to CI/CD.
For WordPress automation, testing, staging, smart updates and more, check out our complete Plesk WP Toolkit.
Test Types
Unit Test – Tests the smallest possible unit of an app. It’s often a single function.
Integration Test – Takes all the unit tests and finds if they work together in the way we’re expecting.
E2E (end-to-end) – Tests the entire path through the organization.
They may cost more to test the higher up the pyramid you go but maybe they take even longer to run. You are after all in many cases making HTTP requests.
SUT (System Under Test)
This refers to the current system we’re trying to test. It can be a single method, a class, or a whole feature. What are we trying to accomplish with our test? And how do we get everything else out of the way so we can focus on that?
When it comes to WP, we do have to shift a little. As we said, it’s a very tightly-coupled system. So, it’s very hard to test single items in true isolation. But this doesn’t mean we can’t do this effectively. And this is what Steve talked about at WCUS 2019.
PHPUnit – Our testing toolbox
Steve talked about PHPUnit by first explaining its structure.
Test Suite – This is a collection of test classes.
Test Class– a collection of one or more test cases.
Test Case – A single scenario you’re going to test.
It’s going to be comprised of one or more assertions. Do things work the way that we expect? Here are a few scenarios that Steve Grunwell highlights.
Is it true or false?
assertTrue () $value ===true?
$this->assertTrue(true);
assertFalse () $value ===false?
$this->assertFalse(false);
Equality
assertEquals() $expected == $actual?
$this->assertEquals($expected, $actual);
assertSame() $expected == $actual?
$this->assertSame($expected, $actual);
Verifying contents of things
assertContains () Does $value contain $expected?
$this->assertContains('b', ['a', 'b', 'c']);
assertRegexp() Does $value match the given $regex?
$this->assertRegexp('/^Fo+/', 'Foo Bar');
Negative assertions
For every assertion, there is a positive and negative assertion.
assertEquals () $expected ==$actual?
assertNotEquals () $expected ==$actual?
assertContains () $expected ==$actual?
assertNotContains () $expected ==$actual?
assertCount () $expected ==$actual?
assertNotCount () $expected ==$actual?
assertArrayHasKey () $expected ==$actual?
assertNotArrayHasKey () $expected ==$actual?
Do we have at least one match? Everything comes down to true or false. The key to understanding assertions in our tests. Here is an example of a test report:
PHPUnit 7.5.1 by Sebastian Bergmann and contributors.
............................................... 47 / 511 ( 9%)
............................................... 94 / 511 ( 18%)
...................................SSSS........ 141 / 511 ( 27%)
............................................... 188 / 511 ( 36%)
............................................... 235 / 511 ( 45%)
............................................... 282 / 511 ( 55%)
............................................... 329 / 511 ( 64%)
............................................... 376 / 511 ( 73%)
............................................... 423 / 511 ( 82%)
............................................... 470 / 511 ( 91%)
......................................... 511 / 511 (100%)
Time: 1.13 minutes, Memory: 42.00MB
OK, but incomplete, skipped, or risky tests!
Tests: 511, Assertions: 1085, Skipped: 4.
PHPUnit 7.5.1 by Sebastian Bergmann and contributors.
.......F........ 16/16 (100%)
Time: 7.15 seconds, Memory: 14.00MB
There was 1 failure:
1) Tests\CoffeeTest::test_get_good_coffee
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'great, well-balanced coffee'
+'Starbucks'
/my-plugin/tests/test-coffee.php:14
FAILURES!
Tests: 16, Assertions: 19, Failures: 1.
It ran through over 1K test in under a minute. If you were to do this manually it would take days instead of minutes.
Test Doubles
As we test things, sometimes we want to get things out of the way in our code. This is where test doubles come into play. The general idea is to remove any variables in our code and give ourselves test versions to replace actual systems. Always returning known values and ensuring systems behave a certain way. When dealing with test doubles, a popular library for creating test doubles is Mockery.
public function test_handles_empty_order_list() {
$api = Mockery::mock( Api::class )->makePartial();
$api->shouldReceive( 'get_all_orders' )
->once()
->andReturn( [] );
$this->assertEmpty( $api->get_recent_orders() );
}
There’s also the PHPUnit Markup assertions, powered by DOMDocument. Lets use DOMDocuments to make a DOM query.
function test_button_contains_active_state() {
$output = some_function();
$this->assertContainsSelector('.button.active', $output);
}
WP Core Test Suite
This is what WP core itself uses to ensure all the PHP in WP is behaving the way we expect it to. If we want to use the core test suite, you can run $ wp scaffold plugin-tests my-plugin to generate test scaffolding via WP-CLI. Get the test suite out of the box.
We want to make sure certain things happen before every test method. You don’t have to write it every time, only once.
We have the concept of groups where we run tests of a similar nature across suites and classes. I can just run the following code.
/**
* @group Posts
* @group PostMeta
*/
public function test_includes_private_posts()
{
// ...
}
$ phpunit --group=Posts
This comes in handy when you have a large test suite and want to make sure related things aren’t going to break.
Data Providers
Often in our testing you can have the same test but different data. For this, we have a nice tool called data providers. You can run through them without having to paste the same method over and over again. So we specify a data provider for it. If you’re working with simple data types like strings and integers. You can choose to define just one method for example:
/**
* @dataProvider my_data_provider()
*/
public function test_my_function( $expected, $value ) {
$this->assertEquals( $expected, my_function( $value ) );
}
public function my_data_provider() {
return [
'Description of case 1' => ['foo', 'bar'],
'Description of case 2' => ['bar', 'baz'],
];
}
/**
* @testWith ["foo", "bar"]
* ["bar", "baz"]
*/
public function test_my_function( $expected, $value ) {
$this->assertEquals( $expected, my_function( $value ) );
}
You can even generate dummy data with factories tests. You can generate users, posts and more – for testing purposes.
// Create the post and retrieve its ID.
$post_id = $this->factory->post->create();
// Create and retrieve the new post.
$post = $this->factory->post->create_and_get();
// Override default parameters.
$post = $this->factory->post->create_and_get( [
'post_title' => 'My Test Post',
'post_author' => $author_id,
] );
// Create multiple instances.
$posts = $this->factory->post->create_many( 5, [
'post_author' => $author_id,
] );
Checking for WP_ERRORS
Was the response an instance of WP_Error? Coming back to the search for truth – Is truth a WP_Error? As we write our code, there’s a pattern for how this should be arranged to set up the scenario.
public function test_function_can_return_wp_error() {
$response = myplugin_function();
$this->assertWPError($response);
}
Next we execute the code, and finally we make assertions around it – in other words, verify that things happened as you expected.
Testing Permissions
public function test_non_admins_cannot_clear_cache() {
// Arrange
$user_id = $this->factory->user->create( [
'role' => 'author',
] );
wp_set_current_user( $user_id );
// Act
$response = myplugin_clear_cache();
// Assert
$this->assertWPError($response);
$this->assertSame(403, $response->get_error_code());
}
Registering a custom post type
public function test_book_cpt_is_registered() {
myplugin_register_post_types();
$post_type = get_post_type_object( 'book' );
// Verify the post type is registered along with key properties.
$this->assertNotNull( $post_type );
$this->assertTrue( $post_type->public );
$this->assertFalse( $post_type->hierarchical );
}
Testing Hooks
public function test_function_does_action() {
myplugin_function();
$this->assertSame( 1, did_action( 'myplugin_action' ) );
}
public function test_function_does_action() {
$called = false;
// Register a callback to validate arguments.
add_action( 'myplugin_action', function () use (&$called) {
// Only return true if validations passed.
$called = true;
} );
myplugin_function();
$this->assertTrue( $called );
}
Testing Output
public function test_shortcode_output() {
ob_start();
do_shortcode( '[recent-posts title="Latest Posts"]' );
$output = ob_get_clean();
$this->assertContains( '<h2>Latest Posts</h2>', $output );
}
public function test_shortcode_output() {
$this->expectOutput( '<h2>Latest Posts</h2>' );
do_shortcode( '[recent-posts title="Latest Posts"]' );
}
Stubbing HTTP Requests
add_filter( 'pre_http_request', function () {
return [
'headers' => [],
'body' => '',
'response' => [
'code' => 200,
'message' => 'OK',
],
'cookies' => [],
'filename' => '',
];
} );
Basic Automated Testing Workflow
Steve explains the basic idea behind TDD – test driven development.
- Write a (failing) test to describe the functionality/behavior. You’re describing how it should work. This can be called ‘red’ – there is a broken code.
- Write the code necessary to make the test pass. All we have to do is get the test to pass. This can be known as green – the code that works.
- Refactor, rinse, & repeat. Now we can go back and refine the code.
Automation is the way forward and one that strongly resonates with Plesk’s values and beliefs. You can find the slide deck from the talk here. Thanks Steve for sharing your expertise on automated testing!
2 Comments
I’m glad your team was able to get a lot out of my talk at #WCUS, and I appreciate you taking the time to write this up.
Two quick things:
1. The link back to https://stevegrunwell.com in the first paragraph appears to be broken.
2. Since a lot of this content comes directly from the slides, could you please also include a link to the slide deck? https://stevegrunwell.com/slides/testing-wordpress
Thanks for attending!
Thanks for your feedback Steve, we have amended both points you mentioned. Cheers once again for the valuable info and look forward to more in the future!