TruUcde Rabbithole 05/12 : Project and Tool config & Sanity Tests

posted in: Development, Series, Testing, truucde, Wordpress | 0
man in white crew neck t-shirt and blue denim jeans standing beside woman in blue
Photo by Jude Beck on Unsplash

In the previous article in the series we first installed PHP Codesniffer and some coding standards. Then we installed PHPUnit and WPMock. What’s we’ll do in this article is make sure that these are all working.

In the case of PHPUnit/WPMock we will need to do a little configuration. We’ll take the opportunity while doing this to organize our project directory to accommodate both unit tests and integration tests.

Let’s start off with the easy part.

Confirming our Code Sniffer

My approach here will be to examine the WordPress coding standards for a couple of easy formatting mistakes to make, set up a dummy file with a small amount of code containing those mistakes and run it through phpcs. If it flags the errors without any operational errors of its own then we know it works. Here’s the file I’ll use:

<?php
add_filter('block_editor_settings', 'jp_block_editor_settings', 10, 2);
function jp_block_editor_settings( $editor_settings, $post ) {
	$editor_settings['autosaveInterval'] = 2000; //number of second [default value is 10]
	
	return $editor_settings;
}

Now we’ll run the phpcs on it with WordPress coding standards:

 vendor/bin/phpcs --standard=WordPress phpcs-dummy.php

And here’s the output:

╰─ vendor/bin/phpcs --standard=WordPress phpcs-dummy.php

FILE: /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/phpcs-dummy.php
-----------------------------------------------------------------------------------------------------------------------------
FOUND 7 ERRORS AFFECTING 5 LINES
-----------------------------------------------------------------------------------------------------------------------------
 1 | ERROR | [ ] Missing file doc comment
 2 | ERROR | [x] Expected 1 spaces after opening parenthesis; 0 found
 2 | ERROR | [x] Expected 1 spaces before closing parenthesis; 0 found
 3 | ERROR | [ ] Missing doc comment for function jp_block_editor_settings()
 4 | ERROR | [x] No space found before comment text; expected "// number of second [default value is 10]" but found
   |       |     "//number of second [default value is 10]"
 4 | ERROR | [ ] Inline comments must end in full-stops, exclamation marks, or question marks
 5 | ERROR | [x] Whitespace found at end of line
-----------------------------------------------------------------------------------------------------------------------------
PHPCBF CAN FIX THE 4 MARKED SNIFF VIOLATIONS AUTOMATICALLY
-----------------------------------------------------------------------------------------------------------------------------

Time: 206ms; Memory: 10MB

The good news is that I have confirmed that phpcs is correctly installed and using the WordPress compatibility standards. I could stop here, but for the sake of closure and completeness I’ll fix the file and run the sniff again.

<?php
/**
 * This file contains utility functions.
 *
 * @package truucde
 */

/**
 * This file contains a function that changes the Gutenberg Editor autosave interval
 * from 10 seconds to 2000 seconds. This helps long files avoid an 'autosave loop'.
 *
 * @param array $editor_settings // Gutenberg editor settings.
 *
 * @return array
 */
function tw_block_editor_settings( $editor_settings ) {
	$editor_settings['autosaveInterval'] = 2000; // number of second [default value is 10].

	return $editor_settings;
}
add_filter( 'block_editor_settings', 'tw_block_editor_settings', 10, 2 );

Okay, this version passes, which means that I get no output from the command. Again for completeness, I’ve done the following in response to the original sniff errors:

  1. Added a file doc comment immediately following the opening php tag. (Note: this uses the phpdoc format. See here for more information on phpdoc: https://docs.phpdoc.org/latest/references/phpdoc/index.html.)
  2. Note that the opening parenthesis in the function declaration and the add filter call now have a space after them for enhanced readability. Close parenthesis now have a space in front of them.
  3. We now have a phpdoc block comment above the function definition.
  4. Inline comment is now space correctly. Inline comment is now terminated correctly.
  5. Whitespace at end of lines removed. A common cause of this is indented blank lines.

Okay, with the sniffer working fine we can proceed with the PHPUnit/WP_Mock pairing.

PHPUnit Setup

Typically, for simple projects test files are put in a tests folder. We are going to be using two different types of tests, unit and integration, that have different set-up requirements. So, we’ll create a tests directory inside of which we’ll create a unit and integration directory, with a tests folder in each of them. Like this:

We’ll need to run the two types of tests separately. It may seem redundant to have a second tests directory inside each of unitTests and integrationTests, but it enables us to keep only tests in the tests subdirectories and other things in the main unitTests and integrationTests directory such as bootstrap files. More about these shortly. Having all of this inside a top level test domain let’s us do things like add it to the .gitignore for distribution.

PHPUnit is a very flexible command-line program. Like most such programs it provides much of its flexibility via command-line options. You can find a complete list here: https://phpunit.readthedocs.io/en/9.2/textui.html#command-line-options.

It would be tedious indeed to have to type all the options you wish to use every time you run the command. Also, it takes a while to learn all the options (I haven’t learned them all yet). Luckily, PHPUnit provides us with a way to create a configuration file. More information about it can be found here: https://phpunit.readthedocs.io/en/9.2/configuration.html. That doesn’t help much, there is still a daunting amount of information.

So, I looked for guidance and found a solid starting point for the XML config file along with some explanation here: https://tommcfarlin.com/phpunit-xml-configuration/. I mentioned Tom McFarlin’s work in the first article of the series and for my money, if Tom thinks this is a good place to start with the XML file that’s good enough for me.

Tom’s article continues to provide a useful tutorial for getting up and going with phpunit, so I’ll use that as a sanity check for the purposes of this article. There are, however, some key differences in our setup that we will have to account for:

  • We have installed phpunit in our project rather than globally, which means we will have to type path information when we run it.
  • We have a different directory structure to eventually provide for both unit testing and integration testing.
  • We will be also including what are called bootstrap files.

This last point Tom refers to in his article:

“This is optional, but if you opt to include other files with your tests (like bringing in a mocking library, part of WordPress, or a third-party library), then this will allow you to define the location of the script that needs to execute. Mocking and bringing in WordPress is outside the scope of this post but it’s something we’ll likely look at in the future as it’s useful when testing plugins.”

We will be doing both of these things, so we’ll need bootstrap files which we will cleverly call bootstrap_unit.php and bootstrap_integration.php which will go in the unitTests and integrationTests directories respectively. I’m going to go ahead and create empty versions of these files that can be filled later. While I’m at it I’ll create an empty XML configuration file for each of the two types of tests. These go in the project root directory and are called: phpunit_unit.xml and phpunit_integration.xml.

The easiest way to create empty files is with the touch command.

touch tests/unitTests/bootstrap_unit.php
touch tests/integrationTests/bootstrap_integration.php

touch phpunit_unit.xml
touch phpunit_integration.xml

And now our directory looks like this (note: PHPStorm colours any file that is new or modified since the last git commit in red):

Ah, I see I need to delete the phpcs-dummy.php from our phpcs sanity test. I’ll do that. Okay, with this basic ground work established let’s set up our phpunit_unit.xml file.

php_unit.xml file

Okay, I’m going to start with Tom McFarlin’s phpunit xml config file and adjust some things for our purposes.

<?xml version="1.0" encoding="UTF-8"?>
<!-- http://phpunit.de/manual/4.1/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
         bootstrap="./tests/unitTests/bootstrap_unit.php"
         backupGlobals="false"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
>
    <testsuites>
        <testsuite name="truucde">
            <directory>./tests/unitTests/tests</directory>
        </testsuite>
    </testsuites>

    <logging>
        <log type="coverage-text" target="php://stdout" showUncoveredFiles="true"/>
    </logging>
</phpunit>

Okay, I have changed the following from Tom’s XML file:

  • the bootstrap file location and name on line 5
  • the testsuite name on line 13
  • the test directory on line 14

With that in place we can turn to the bootstrap file.

PHPUnit Bootstrap file

Remember from Tom’s quote above that the bootstrap file is useful for bringing in a mocking library (which we need to do here) or parts of WordPress (which we will do later for integration testing). So, it’s probably a good idea to check the WPMock documentation to see if there are any bootstrap file requirements. https://github.com/10up/wp_mock

Aha. The WPMock documentation tells us exactly what we need in our bootstrap file. So I’ll put that stuff in.

<?php

// First we need to load the composer autoloader so we can use WP Mock
require_once __DIR__ . '/vendor/autoload.php';

// Now call the bootstrap method of WP Mock
WP_Mock::bootstrap();

/**
 * Now we include any plugin files that we need to be able to run the tests. This
 * should be files that define the functions and classes you're going to test.
 */
require_once __DIR__ . '/sanity-test.php';

The only thing I changed was the file name in line 13. This is temporary for our sanity-test, once we confirm things are working I’ll change it to the plugin file truucde.php. Many plugins that you’ll test involve more than one plugin file or directory, there are different ways to require those that we won’t cover here, Tom outlines such a method in his 3. The Bootstrap File section, but our needs are different, so we’ll ignore that for now.

What we’ll do here is adapt his approach and code in the rest of his article to conduct our sanity test. He starts by writing the test, rather than the code. This is part of an approach called Test Driven Development (TDD). It is beyond the scope of this article series, but is a solid approach to development with many benefits. The basic idea is to write/figure out the test before the next bit of code. The test will fail the first time without the code, but you code and refactor until the test succeeds, then do the next bit of code. A more complete description can be found here: https://en.wikipedia.org/wiki/Test-driven_development.

We’ll follow along Tom’s article this way because it is useful for us to see a test fail. It is part of showing us whether phpunit is working correctly or not. So, first thing is to run phpunit to see what happens. Remembering that we have installed locally rather than globally, we’ll need to include the path when we run phpunit (which we always do from the project root directory). We also need to call phpunit with our configuration file.

vendor/bin/phpunit -c phpunit_unit.xml

Zoot Alor, look what happened with our output:

PHP Warning:  require_once(/Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/tests/unitTests/vendor/autoload.php): failed to open stream: No such file or directory in /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/tests/unitTests/bootstrap_unit.php on line 4
PHP Stack trace:
PHP   1. {main}() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/phpunit:0
PHP   2. PHPUnit\TextUI\Command::main() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/phpunit:61
PHP   3. PHPUnit\TextUI\Command->run() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/TextUI/Command.php:162
PHP   4. PHPUnit\TextUI\Command->handleArguments() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/TextUI/Command.php:173
PHP   5. PHPUnit\TextUI\Command->handleBootstrap() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/TextUI/Command.php:863
PHP   6. PHPUnit\Util\FileLoader::checkAndLoad() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/TextUI/Command.php:1058
PHP   7. PHPUnit\Util\FileLoader::load() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/Util/FileLoader.php:45
PHP   8. include_once() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/Util/FileLoader.php:57

Warning: require_once(/Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/tests/unitTests/vendor/autoload.php): failed to open stream: No such file or directory in /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/tests/unitTests/bootstrap_unit.php on line 4

Call Stack:
    0.0006     402672   1. {main}() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/phpunit:0
    0.0037     834152   2. PHPUnit\TextUI\Command::main() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/phpunit:61
    0.0037     834264   3. PHPUnit\TextUI\Command->run() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/TextUI/Command.php:162
    0.0037     834264   4. PHPUnit\TextUI\Command->handleArguments() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/TextUI/Command.php:173
    0.0220    1108640   5. PHPUnit\TextUI\Command->handleBootstrap() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/TextUI/Command.php:863
    0.0225    1114200   6. PHPUnit\Util\FileLoader::checkAndLoad() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/TextUI/Command.php:1058
    0.0228    1114616   7. PHPUnit\Util\FileLoader::load() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/Util/FileLoader.php:45
    0.0230    1116336   8. include_once('/Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/tests/unitTests/bootstrap_unit.php') /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/Util/FileLoader.php:57

PHP Fatal error:  require_once(): Failed opening required '/Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/tests/unitTests/vendor/autoload.php' (include_path='.:/usr/local/Cellar/php/7.4.1/share/php/pear') in /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/tests/unitTests/bootstrap_unit.php on line 4
PHP Stack trace:
PHP   1. {main}() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/phpunit:0
PHP   2. PHPUnit\TextUI\Command::main() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/phpunit:61
PHP   3. PHPUnit\TextUI\Command->run() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/TextUI/Command.php:162
PHP   4. PHPUnit\TextUI\Command->handleArguments() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/TextUI/Command.php:173
PHP   5. PHPUnit\TextUI\Command->handleBootstrap() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/TextUI/Command.php:863
PHP   6. PHPUnit\Util\FileLoader::checkAndLoad() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/TextUI/Command.php:1058
PHP   7. PHPUnit\Util\FileLoader::load() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/Util/FileLoader.php:45
PHP   8. include_once() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/Util/FileLoader.php:57

Fatal error: require_once(): Failed opening required '/Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/tests/unitTests/vendor/autoload.php' (include_path='.:/usr/local/Cellar/php/7.4.1/share/php/pear') in /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/tests/unitTests/bootstrap_unit.php on line 4

Call Stack:
    0.0006     402672   1. {main}() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/phpunit:0
    0.0037     834152   2. PHPUnit\TextUI\Command::main() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/phpunit:61
    0.0037     834264   3. PHPUnit\TextUI\Command->run() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/TextUI/Command.php:162
    0.0037     834264   4. PHPUnit\TextUI\Command->handleArguments() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/TextUI/Command.php:173
    0.0220    1108640   5. PHPUnit\TextUI\Command->handleBootstrap() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/TextUI/Command.php:863
    0.0225    1114200   6. PHPUnit\Util\FileLoader::checkAndLoad() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/TextUI/Command.php:1058
    0.0228    1114616   7. PHPUnit\Util\FileLoader::load() /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/Util/FileLoader.php:45
    0.0230    1116336   8. include_once('/Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/tests/unitTests/bootstrap_unit.php') /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/vendor/phpunit/phpunit/src/Util/FileLoader.php:57

This is just the kind of configuration error that we do the sanity test for. In fact, it looks like the same error repeating (with the stack trace) a few times. So, what it is telling us is that it can’t open the file identified on line 4 of our bootstrap file. Let’s examine the path of the file it’s trying to load. Ah, it’s looking for the vendor directory in the unitTests directory rather than the project root directory. Note that line 4 of the bootstrap_unit.php file has __DIR__ . '/vendor/autoload.php'. __DIR__ is a ‘magic constant’ for the directory that the calling file, our bootstrap file, is currently in. We don’t want that, our vendor directory is in the project root, which is our current directory at the command line. Also, the slash before vendor will take us to the file system root, rather than the project root, so we need to get rid of this as well.

So, we run the phpunit command again. That gets rid of that error, but now we have a similar error for the call to sanity-test.php. This is the same issue. Our sanity-test.php file (which is our code file) will be in the project root, so we fix it the same way by removing the __DIR__ and the leading slash. And run the command again, it still is showing file not found. Doh! That’s because I haven’t created the file yet. Okay, create an empty file and run the command again.

vendor/bin/phpunit -c phpunit_unit.xml
PHPUnit 7.5.20 by Sebastian Bergmann and contributors.

Error:         Incorrect whitelist config, no code coverage will be generated.



Time: 54 ms, Memory: 4.00 MB

No tests executed!

Okay, now we’re getting somewhere, PHPUnit ran, but we had no tests to execute. The error that it is showing relates to the <logging> section of our phpunit_unit.xml file that provides a code coverage report. This type of report helps show how much of the code is covered by tests. We can ignore this error for now and carry on.

The PHPUnit Sanity Test

Okay, what Tom has us do next is write a test, which should fail as there is no code to test. So let’s create our test file. Test files in php unit need to be the same as the code file with Test added to the filename. So, for our sanity-test.php file we’ll create sanity-testTest.php in our tests folder. Normally I type tutorial text in because it helps get my head around the code, but in this case I’m just going to copy it.

<?php

namespace AcmeTests;

use PHPUnit\Framework\TestCase;
use Acme\AcmeCache;

class AcmeCacheTest extends TestCase
{
	private $cache;
	
	public function setUp()
	{
		$this->cache = new AcmeCache();
	}
	
	public function testCacheExists()
	{
		$this->assertNotNull($this->cache);
	}
}

Okay, some notes here.

  • The command use PHPUnit\Framework\TestCase; loads up the business end of PHPUnit for us. (line5) We then extend this class in line 8 for our own test class. This makes all the magic of PHPUnit available to us.
  • The setUp function on line 12 creates a context for our test. Specifically it creates a instance of the class that will be defined in our code file so that we can use it for testing. It stores that new object in the $cache property defined on line 10.
  • Line 17 defines our first test function where we test to see if the object exists using PHPUnit’s assertNotNull assertion.

And we run our command again.

vendor/bin/phpunit -c phpunit_unit.xml
PHPUnit 7.5.20 by Sebastian Bergmann and contributors.

Error:         Incorrect whitelist config, no code coverage will be generated.

E                                                                   1 / 1 (100%)

Time: 31 ms, Memory: 4.00 MB

There was 1 error:

1) AcmeTests\AcmeCacheTest::testCacheExists
Error: Class 'Acme\AcmeCache' not found

/Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/tests/unitTests/tests/sanity-testTest.php:14

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

Ignoring the whitelist config error again, we see that we have a testing error. Class not found. This is to be expected as we haven’t created our code yet. The :14 at the end of the error is the line number where the error occurred. On that line we are trying to create an instance of a class that doesn’t exist, hence the error. This is typical of failing test output. So, let’s get the class created. Note: that in any situation other than a sanity test, the file name would be the same as the class.

<?php

namespace Acme;

class AcmeCache
{
    private $duration;

    public function __construct()
    {
        $this->duration = 43200;
    }

    public function setDuration(int $duration)
    {
        $this->duration = $duration;
    }

    public function getDuration(): int
    {
        return $this->duration;
    }
}

Then we run our phpunit command again.

vendor/bin/phpunit -c phpunit_unit.xml
PHPUnit 7.5.20 by Sebastian Bergmann and contributors.

Error:         Incorrect whitelist config, no code coverage will be generated.

.                                                                   1 / 1 (100%)

Time: 51 ms, Memory: 4.00 MB

OK (1 test, 1 assertion)

Again, we ignore the whitelist error and see that our test passes. So that means our sanity check with respect to PHPUnit has passed, we have PHPUnit running correctly. But what about WP Mock, we need a sanity test for it. Let’s do that next.

WPMock Sanity Test

Okay, we’ll empty out and use the same sanity test files that we used for the PHPUnit sanity test. We only use them once at the beginning of a project and then delete them. We’ll look at the WPMock documentation and see if they have a simple test and corresponding code we can use, and they do. Note: I’m not trying to weasel out of writing this stuff myself, but I find when using a new thing and building sanity tests that provided examples are a good and safe way to proceed, because clearly they worked for the provider. It removes one element of uncertainty.

Okay, let’s look at the code function under test first.

/**
 * Get a post's permalink, then run it through special filters and trigger
 * the 'special_action' action hook.
 *
 * @param int $post_id The post ID being linked to.
 * @return str|bool    The permalink or a boolean false if $post_id does
 *                     not exist.
 */
function my_permalink_function( $post_id ) {
	$permalink = get_permalink( absint( $post_id ) );
	$permalink = apply_filters( 'special_filter', $permalink );

	do_action( 'special_action', $permalink );

	return $permalink;
}

Okay, so a few notes before we look at the test code.

  • The function comment doc block tells us what the function does, what parameter input it needs and what it returns.
  • We will be testing this in isolation from any WordPress code, so any WordPress functions will need to be mocked. These are:
    • get_permalink() and absint() in line 11
    • applyfilters() in line 12
    • do_action in line 14

Okay, we’ll leave it at that for now and look at the test code:

<?php

class MyTestClass extends \WP_Mock\Tools\TestCase {
	public function setUp() {
		\WP_Mock::setUp();
	}
	
	public function tearDown() {
		\WP_Mock::tearDown();
	}
	
	/**
	 * Assume that my_permalink_function() is meant to do all of the following:
	 * - Run the given post ID through absint()
	 * - Call get_permalink() on the $post_id
	 * - Pass the permalink through the 'special_filter' filter
	 * - Trigger the 'special_action' WordPress action
	 */
	public function test_my_permalink_function() {
		\WP_Mock::userFunction( 'get_permalink', array(
			'args' => 42,
			'times' => 1,
			'return' => 'http://example.com/foo'
		) );
		
		\WP_Mock::passthruFunction( 'absint', array( 'times' => 1 ) );
		
		\WP_Mock::onFilter( 'special_filter' )
		        ->with( 'http://example.com/foo' )
		        ->reply( 'https://example.com/bar' );
		
		\WP_Mock::expectAction( 'special_action', 'https://example.com/bar' );
		
		$result = my_permalink_function( 42 );
		
		$this->assertEquals( 'https://example.com/bar', $result );
	}
}

Okay, let’s go through this before running the test.

  • In our PHPUnit test our test class extended PHPUnit TestCase. Here we are extending \WP_Mock\Tools\TestCase. We can actually go have a look at this file if we are curious at /vendor/10up/wp_mock/php/WP_Mock/Tools/TestCase.php. What is interesting is that the class in this file extends the PHPUnit TestCase. So basically, WP_Mock adds its goodies to the PHPUnit TestCase and we use the result for our tests which gives us access to the functionality of both PHPUnit and WP_Mock.
  • Sometimes documentation on things will be a bit thin, but we can always go into the code and read the doc blocks to learn what functions are available and what they do.
  • A popular testing conceptual model is called AAA: Arrange, Act, Assert. I’ll talk about this more in upcoming articles, but the basic idea is that you arrange the conditions of the test. Act on the target code. Assert that the result is the same as expected.
  • Part of the Arrange in testing is often using what are called Setup and Teardown functions to Arrange part of the context and then clear it out on finish. We see in lines 4 through 10 this idea in action where WP_Mock setup routines are run and then they are cleared out afterwords. This basically makes WP_Mock available in the test file.
  • Lines 20 to 32 create the mocks for the 4 WordPress functions. The Mocks imitate the WordPress functions in order to test the code logic.
  • Line 34 is the test run of the target function with a supplied value. This is the Act part of the AAA model. Note that this value matches the ‘args’ item in the first mock along with what to return. This is then filtered in the third (onFilter mock).
  • Line 36 is the Assert part of the AAA model. It basically says, if the code is working properly the end result ($result) should be ‘https://example.com/bar’.

Okay, let’s run it and see if everything is hooked up properly. We use the same PHPUnit command as before. And I get the following error:

PHP Fatal error:  Declaration of MyTestClass::setUp() must be compatible with WP_Mock\Tools\TestCase::setUp(): void in /Users/twelch/www/sites/multibox/wwwroot/wp-content/plugins/truucde-blog/tests/unitTests/tests/sanity-testTest.php on line 4

I see that WP_Mock\Tools\TestCase::setUp(): void in the error has the :void at the end of it. This is a type hint that specifies the return value of the function. I checked in the code for WP_Mock::setup() and it has this type hint. This means that we have to add this type hint to our higher level Setup() function. I did that and then, predictably got the same error for tearDown. Just for completeness, our setup and teardown functions now look like this:

	public function setUp(): void {
		\WP_Mock::setUp();
	}

	public function tearDown(): void {
		\WP_Mock::tearDown();
	}

Fixed that and then ran our command again.

vendor/bin/phpunit -c phpunit_unit.xml
PHPUnit 7.5.20 by Sebastian Bergmann and contributors.

Error:         Incorrect whitelist config, no code coverage will be generated.

.                                                                   1 / 1 (100%)

Time: 143 ms, Memory: 6.00 MB

OK (1 test, 1 assertion)

Excellent, it passed. That’s our WP_Mock sanity test completed. I can now deleted the sanity files and edit the bootstrap_unit.php file to require our code under test, the truucde.php file. As a final step, I will do a Git commit to add our testing config files. Same set of commands as in the last article.

git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	phpunit_integration.xml
	phpunit_unit.xml
	tests/

nothing added to commit but untracked files present (use "git add" to track)

git add -A

git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	new file:   phpunit_integration.xml
	new file:   phpunit_unit.xml
	new file:   tests/integrationTests/bootstrap_integration.php
	new file:   tests/unitTests/bootstrap_unit.php


git commit -m "Unit test config files and integration conf placeholders"
[master 673ac7a] Unit test config files and integration conf placeholders
 4 files changed, 34 insertions(+)
 create mode 100644 phpunit_integration.xml
 create mode 100644 phpunit_unit.xml
 create mode 100644 tests/integrationTests/bootstrap_integration.php
 create mode 100644 tests/unitTests/bootstrap_unit.php

Wrapping up

Okay, we covered a lot of ground here. Although, we always seem to in this article series. First we ran a quick test to make sure we had PHP Codesniffer working.

We created our PHPUnit config xml file which sets up our PHPUnit calls. We created a bootstrap_unit.php file which hooks in WP_Mock before we run our tests.

We did sanity tests for both pure PHP Unit and PHP Unit with WP_Mock to check and make sure we had everything hooked up right. We relied on the examples of other authors to help ensure that the code and tests were at least correct.

Along the way we look at some important concepts, two of which I would like to highlight. First is the AAA testing model: Arrange, Act, Assert. When you are designing and troubleshooting tests this paradigm can be a useful guide. Second is that we can look into vendor code that we are relying on and find that it is usually self-documenting via comments (hopefully in phpdoc format). We can also just read the code, this is a useful practice to get in to. It can point to good practices we can use in our own code.

The one thing left dangling is the whitelist configuration error we’ve been ignoring in our unit tests. We’ll take a look at that and code coverage reporting in the next article, see you there.

Comments are closed.