Automated tests are the base of modern programming development process and are as important as the architecture from the code itself.
As they are what prevent regression in your project preventing your project to crumble on itself.
In this article, we will focus more particularly on unit test.
The main objective from an unit test is to make sure everything is working well inside a class due to this our tests will always focus on one class.
For this we will use the most used PHP test library PHPUnit but other libraries exists and tools we will use in this article adapt to most of the frameworks.
Configure PHPUnit
The first step when creating unit tests is to install our environment.
For that we will first to create a Composer project if this is not done.
On Ubuntu, we can install Composer with the following command:
sudo apt install composer
Once this is done, you can move to the project folder (plugin or theme folder) and run this command to create the Composer project:
composer init
Once this is done a file called composer.json should have been created.
If so we can start to add PHPUnit to the project with the following command:
composer require --dev phpunit/phpunit
Then we can install the project with the following command:
composer install
Once this step is done a new file called composer.lock and a vendor folder should have been created.
At this level PHPUnit is installed but not configured into your project.
For that, we will first create a folder tests that we will fill with the files used to create tests and configure PHPUnit.
In that folder tests we will create another folder Unit that will be used for our unit tests. This is not mandatory but it will organize your tests better as there is not only unit tests and it is more practical to separate them.
In that folder we will have to create 3 files to configure PHPUnit:
- TestCase.php: The base class for all of your test classes.
- bootstrap.php: This file will load every resource necessary for the tests.
- phpunit.xml.dist: Configuration file from PHPUnit.
We will start by creating the TestCase.php file where we will add this content to extend our cases from PHPUnit TestCase class:
<?php
use PHPUnit\Framework\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
}
Then we will write the following content into bootstrap.php to load our code and the base TestCase:
<?php
require_once dirname(__DIR__) . '/../vendor/autoload.php';
require_once __DIR__ . '/TestCase.php';
Finally we add to configure where are tests and the bootstrap file:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.3/phpunit.xsd"
bootstrap="bootstrap.php"
backupGlobals="false"
colors="true"
beStrictAboutCoversAnnotation="false"
beStrictAboutOutputDuringTests="true"
beStrictAboutTestsThatDoNotTestAnything="true"
beStrictAboutTodoAnnotatedTests="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
verbose="true">
<testsuites>
<testsuite name="unit">
<directory suffix=".php">src</directory>
</testsuite>
</testsuites>
</phpunit>
Once we did that we can add a Test.php in a src folder inside Unit folder with this content to verify everything is well configured:
class Test extend TestCase {
public function testShouldReturnTrue() {
$this->assertTrue(true);
}
}
Then run the following command from the root of your project:
./vendor/bin/phpunit --configuration ./tests/Unit/phpunit.xml.dist
It should return you result like this one:
PHPUnit 9.5.20 #StandWithUkraineRuntime: PHP 8.0.18
Configuration: ./tests/Unit/phpunit.xml.dist
Warning: Your XML configuration validates against a deprecated schema.
Suggestion: Migrate your XML configuration using "--migrate-configuration"!. 1/ 1(100%)Time: 00:00.015, Memory: 6.00 MBOK (1 tests, 1 assertions)
Mock WordPress functions
Now that we have configure PHPUnit, we will now focus on a problem more linked to WordPress itself.
As a WordPress developer you know for sure that plugins and themes are based on global functions.
Theses functions are a problem when testing as they are not defined when we tests.
For that a library will help us, Brain Monkey that allow to mock global functions easily.
To do install this library, run this command:
composer require brain/monkey --dev
Once this command is executed , we need to change a bit the TestCase.php file as Brain Monkey needs to know when the test start or finish:
<?php
use PHPUnit\Framework\TestCase as BaseTestCase;
use Brain\Monkey;abstract class TestCase extends BaseTestCase
{
protected function setUp() {
parent::setUp();
Monkey\setUp();
}
protected function tearDown() {
Monkey\tearDown();
parent::tearDown();
}
}
Once the library is configured, lets see how to use it.
Brain Monkey offer a lot of way to mock different types of functions:
- Actions linked functions which are defined when loading Brain Monkey and for which an facade is available to mock them.
- Filters linked functions which are defined when loading Brain Monkey and for which an facade is available to mock them.
- Other functions for which an interface is available to mock them.
To mock a global function with Brain Monkey we first need to import the facade then use the expect function:
use Brain\Monkey\Functions;Functions\expect('a_global_function');
It is possible to specify parameters with the with function or a result with andReturn function:
use Brain\Monkey\Functions;Functions\expect('a_global_function')->with('param1', 'param2')->andReturn(11);
As you might notice Brain Monkey use the same functions as Mockery as it is based on it.
Use fixtures
The next point I want to cover with you about testing is fixtures which is a functionality that will allowing you to save ton of time maintaining your unit tests.
Fixtures are a functionality under PHPUnit that allow a programmer to pass parameters to a test function changing the behavior of the test without having to copy paste the code. This reducing the amount of code the developer has to write.
In this part, we will configure and create an example test using fixtures.
To configure fixtures the first step is to create a new folder in tests folder named Fixtures that will contain our fixtures.
Then the second step will be to change the TestCase.php file to add methods to load fixtures for each tests:
<?php
use PHPUnit\Framework\TestCase as BaseTestCase;
use ReflectionObject;
abstract class TestCase extends BaseTestCase
{
public function configTestData() {
if (empty($this->config)) {
$this->loadTestDataConfig();
}
return isset( $this->config['test_data'] )
? $this->config['test_data']
: $this->config;
}
protected function loadTestDataConfig() {
$obj = new ReflectionObject($this);
$filename = $obj->getFileName();
$this->config = $this->getTestData(dirname($filename), basename($filename, '.php'));
}
protected function getTestData( $dir, $filename ) {
if ( empty( $dir ) || empty( $filename ) ) {
return [];
}
$dir = str_replace('Unit', 'Fixtures', $dir);
$dir = rtrim($dir, '\\/');
$testdata = "$dir/{$filename}.php";
return is_readable($testdata)
? require $testdata
: [];
} protected function setUp() {
parent::setUp();
Monkey\setUp();
}
protected function tearDown() {
Monkey\tearDown();
parent::tearDown();
}
}
With our new TestCase we can now add a provider on our test to load the Fixture:
class Test extend TestCase { /**
* @dataProvider configTestData
*/
public function testShouldReturnTrue($config, $expected) {
$this->assertEquals($config['value'], $expected);
}
}
Now to create our fixture file we just need to create a file with the same path (Fixtures/src/Test.php) as the test in the Fixtures folder containing an array with data to pass to the test:
<?php
return [
'scenario1' => [
'config' => [
'value' => true,
],
'expected' => true,
],
'scenario2' => [
'config' => [
'value' => false,
],
'expected' => false,
],
];
Once this is done our fixtures are configure and we can run the command to launch PHPUnit again:
./vendor/bin/phpunit --configuration ./tests/Unit/phpunit.xml.dist
It should return you result like this one:
PHPUnit 9.5.20 #StandWithUkraineRuntime: PHP 8.0.18
Configuration: ./tests/Unit/phpunit.xml.dist
Warning: Your XML configuration validates against a deprecated schema.
Suggestion: Migrate your XML configuration using "--migrate-configuration"!.. 2 / 2 (100%)Time: 00:00.015, Memory: 6.00 MBOK (2 tests, 2 assertions)
Make your classes easier to test
Now that we configure our tests well we will start to look around it and the first point will be to make your code easier to test.
Today WordPress developers have some practices that make testing more complex.
The first practice is to write filters and actions in the constructor from the classes. This is problematic because it force the developer to mock actions and filters for tests form all functions from the class.
Instead registering actions and filters in another method that is called after the class is initialized allow to test method from the class easier.
Another practice is to add global PHP inside the class file forcing the code to be executed when we load the file.
For that the solution I preconise is to follow PSR-4 conventions as much as you can in your code:
- Only one class per file.
- Use namespaces.
- Split PSR-4 files from the others.
Calculate coverage
Once we have tests the next step is to know which code we tested which we did not and for that a tool called code coverage exists.
However this tool has some problem.
When a test is covering a line this tool won’t try to see if there are multiple logical path on this line and will just mark the line as covered even if we did only test it one way.
This problem is easy to see with a simple if:
if($a == 1 || $b == 2) {
Imagine we have a test that test with $a = 1 then the second part of the line won’t be tested but the line will be marked as covered.
To prevent this problem I prefer using another tool to do coverage named mutation testing. Even if its name looks like more tests to write you have if fact nothing to do expect running the library and then the library will provide you a coverage from your code.
How this tool is different from vanilla code coverage?
Here instead of check if a unit test passed by the line, the tool will create mutant based on a variation in the code which can be mutation from the visibility from a function, changing an operator or removing the call to a function.
Then it will check if with your test each mutation is failing and if it is not the case the percentage of mutation that passed your tests giving you a coverage from your code.
In PHP, a library that provide us mutation tests is Infection and in this part we will see how to install it and use it.
To install it, we can use Composer to install it with this command:
composer require infection/infection --dev
Then the next step is to configure it by creating infection.json.dist
file with this content:
{
"source": {
"directories": [
"src"
]
},
"phpUnit": {
"configDir": "tests\/Unit"
},
"mutators": {
"@default": true
}
}
Then you just have to run the following command and Infection will tell you which problems are present in your tests:
./vendor/bin/infection
Automate tests run
Once we finished our unit tests setup, we will automate what can we can.
For that we will use Composer Git Hooks that will allow us to handle Git hooks from composer.json file.
The first will be to simply commands we run with Composer.
For that will have to add the following lines in your composer.json:
"scripts": {
"unit": "phpunit --configuration tests/Unit/phpunit.xml.dist",
"coverage": "infection --min-msi=50"
}
Now we can run unit tests with this command:
composer run unit
And mutation tests with this command (the command will fail if the coverage is inferior to 50%):
composer run coverage
We will now install Composer Git Hooks with the following command:
composer require --dev brainmaestro/composer-git-hooks
And configure it by adding this to composer.json:
"scripts": {
"unit": "phpunit --configuration tests/Unit/phpunit.xml.dist",
"coverage": "infection --min-msi=50",
"cghooks": "cghooks",
"post-install-cmd": "cghooks add --ignore-lock",
"post-update-cmd": "cghooks update"
},
"extra": {
"hooks": {
"config": {
"stop-on-failure": ["pre-push"]
},
"pre-push": [
"unit",
"coverage"
]
}
}
We will now have to run manually the following command to install hooks:
composer run post-install-cmd
However with post-install-cmd and post-update-cmd hooks will be installed by themself on any composer install or update.
Now every time we push some code both unit test and coverage is runned automatically.
If we want to skip theses hooks we can use -n on your git command.
That’s it that all for this first article on testing on WordPress, I hope you enjoy it and if you are interested I will publish new ones on Integration testing and Acceptance testing on WordPress.
Laisser un commentaire