Setup integration tests for your WordPress Plugin

A while ago I created a first article about unit tests nearly 2 years ago promising for a next article on integration tests.

It took a while and my vision changed a lot about tests during that period of time.

A vision shift

While writing the article on unit tests I was convinced unit tests where the first to learn to write. However, the fragility from theses tests made me change my mind as they weren’t giving enough results for new developers to convince them to keep using them on the long term.

This is why I slowly changed my mind and finally started recommending to developers to begin by focusing on the most stable tests, integration tests, and that even if they are more complex that unit tests to start with.

All of this is what pushed me into writing this article to teach the base of integration tests to developers wanting start testing as creating the environment to test is often the most complex part.

But first to understand well what we will be doing it is important to get the main differences between unit and integration tests.

Unit tests vs Integration tests

Where unit tests are supposed to test the classes or methods individually as their name let it guess, on the other side integration tests will be on an higher level testing at the level from the components or features.

Being at features level an advantage as now it is possible to use business assertions to test our code and it is not any longer up to us the developer to find cases from our tests.

At the same time testing an higher level also means higher abstraction leading into more flexibility to change and less fragile tests.

Theses two points makes theses tests a strong candidate to start with and stick to on the long term.

Now that know what are integration tests and why they are the best choice to start with it is time to install the environment.

Configure the testing environment

To not repeat the process I will consider that you already have a composer project initialized.

If it is not the case you can follow the steps detailed in my article on Unit tests.

As setup a full environment for integration tests can be long and complex if done manually we will have rely on some libraries to make the job for us.

wordpress/env

As setting up a developing environment is something that can be time wasting WordPress community developed an automated way to setup one.

As you might guess the name from that tool is wordpress/env but before using it make sure you have Docker installed.

Once this is done the next requirement is to have npm, the Node.js package manager, installed. If it is not the case you can find a tutorial here.

With theses requirements met then the installation can start.

First a new Node.js project need to be initialized at the root from our plugin project with the following command:

npm init

This should generate a new package.json file into the folder.

Then the next step will be to install wordpress/env with the following command:

npm i wordpress/env

Once this is completed we will have to add the following content inside package.json:

{
"scripts": {
"wp-env:start": "wp-env start",
"wp-env:stop": "wp-env stop",
"wp-env:destroy": "wp-env destroy"
},
}

Finally the last step is to run the environment using this command:

npm run wp-env:start

If everything goes fine then it should give the following output:

> wp-env:start
> wp-env start

⚠ Warning: could not find a .wp-env.json configuration file and could not determine if '/var/www/testing-wp/web/app/plugins/my_plugin' is a WordPress installation, a plugin, or a theme.
WordPress development site started at http://localhost:8888
WordPress test site started at http://localhost:8889
MySQL is listening on port 32770
MySQL for automated testing is listening on port 32769

✔ Done! (in 57s 413ms)

Process finished with exit code 0

wp-media/phpunit

Once the development environment is settled the next step is to setup the tests themselves.

For that we will delegate most of the work to the library wp-media/phpunit which gonna setup and reset the environment for us.

The first to use wp-media/phpunit is to install the library by running the following command:

composer i wp-media/phpunit --dev

wp-launchpad/phpunit-wp-hooks

In the WordPress ecosystem integration tests mocking filters is something really common due to that it is really important to make sure that operation is the less verbose as possible.

The library wp-launchpad/phpunit-wp-hooks is done to reduce the amount of code to interact with a filter.

To install that library that library you need to run the following command:

composer i wp-launchpad/phpunit-wp-hooks --dev

Once this is done the library is installed it is now time to create base classes for tests.

Make the base

The first step will be to create the namespace inside the composer.json file from the project by adding the following code inside:

"autoload-dev": {
"psr-4": {
"MyPlugin\\Tests\\Integration\\": "Integration/"
}
},

If it is not the case inside the project we will have to create a new folder tests and within that folder another one named Integration.

Then the next step is to create file init-tests.php inside the Integration folder. The objective from that file is to setup wp-media/phpunit library by indication the position from the testing folder:

<?php
/**
* Initializes the wp-media/phpunit handler, which then calls the rocket integration test suite.
*/

define( 'WPMEDIA_PHPUNIT_ROOT_DIR', dirname( __DIR__ ) . DIRECTORY_SEPARATOR );
define( 'WPMEDIA_PHPUNIT_ROOT_TEST_DIR', __DIR__ );
require_once WPMEDIA_PHPUNIT_ROOT_DIR . 'vendor/wp-media/phpunit/Integration/bootstrap.php';

define( 'WPMEDIA_IS_TESTING', true ); // Used by wp-media/{package}.

Once this is done then we need to create another file bootstrap.php which gonna setup initial environment for our tests:

<?php

namespace MyPlugin\Tests\Integration;

define( 'MY_PLUGIN_PLUGIN_ROOT', dirname( dirname( __DIR__ ) ) . DIRECTORY_SEPARATOR );
define( 'MY_PLUGIN_TESTS_DIR', __DIR__ );

// Manually load the plugin being tested.

Finally PHPUnit should be configured to execute the suite.

For that we will have to add the following content into phpunit.xml.dist :

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" bootstrap="init-tests.php" backupGlobals="false" colors="true" beStrictAboutCoversAnnotation="false" beStrictAboutOutputDuringTests="true" beStrictAboutTestsThatDoNotTestAnything="true" beStrictAboutTodoAnnotatedTests="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" verbose="true">
<coverage includeUncoveredFiles="true">
<include>
<directory suffix=".php">../../inc</directory>
</include>
</coverage>
<testsuites>
<testsuite name="integration">
<directory suffix=".php">inc</directory>
</testsuite>
</testsuites>
</phpunit>

Finally we will have to create a base TestCase class.

It will be used to contain logic which will be common to each of our tests.

For that we will add the following content into TestCase.php where my_prefix is your plugin prefix:

namespace MyPlugin\Tests\Integration;

use WPMedia\PHPUnit\Integration\TestCase as BaseTestCase;
use WPLaunchpadPHPUnitWPHooks\MockHooks;

abstract class TestCase extends BaseTestCase
{
use MockHooks;

public function set_up() {
parent::set_up();

$this->mockHooks();
}

public function tear_down()
{
$this->resetHooks();

parent::tear_down();
}

function getPrefix(): string
{
return 'my_prefix';
}

function getCurrentTest(): string
{
return $this->getName();
}
}

Finally the last step is to add the script to launch integration tests inside composer.json :

"test-integration": "\"vendor/bin/phpunit\" --testsuite integration --colors=always --configuration tests/Integration/phpunit.xml.dist --exclude-group AdminOnly,,",

And add the script to run the previous script inside package.json where my_plugin is the name from the directory from your plugin:

"integration": "wp-env run cli --env-cwd=wp-content/plugins/my_plugin composer run test-integration",

It is now possible execute the tests by running the following command:

npm run integration

If everything goes fine you should have the following output:

> integration
> wp-env run cli --env-cwd=wp-content/plugins/my_plugin composer run test-integration

ℹ Starting 'composer run test-integration' on the cli container.

> "vendor/bin/phpunit" --testsuite integration --colors=always --configuration tests/Integration/phpunit.xml.dist
Installing...
Running as single site... To run multisite, use -c tests/phpunit/multisite.xml
Not running ajax tests. To execute these, use --group ajax.
Not running ms-files tests. To execute these, use --group ms-files.
Not running external-http tests. To execute these, use --group external-http.
PHPUnit 9.6.17 by Sebastian Bergmann and contributors.

Runtime: PHP 8.2.15
Configuration: tests/Integration/phpunit.xml.dist

No tests executed!
✔ Ran `composer run test-integration` in 'cli'. (in 5s 632ms)

Process finished with exit code 0

Use fixtures

To fully understand the importance of fixtures you can check my previous article about unit tests where I already explained the advantages of using them.

In this article I will show how to make your tests compatible with fixtures and this time it is even simpler than with unit tests as wp-media/phpunit is handling a part of the complexity for us.

The first part will be to add the Fixture folder inside the tests folder.

Then the second part will be to add the logic to load fixtures inside the TestCase class:

namespace MyPlugin\Tests\Integration;

use WPMedia\PHPUnit\Integration\TestCase as BaseTestCase;
use WPLaunchpadPHPUnitWPHooks\MockHooks;

abstract class TestCase extends BaseTestCase
{
use MockHooks;

protected $config;

public function set_up() {
parent::set_up();

if ( empty( $this->config ) ) {
$this->loadTestDataConfig();
}

$this->mockHooks();
}

public function tear_down()
{
$this->resetHooks();

parent::tear_down();
}

public function getPrefix(): string
{
return 'my_prefix';
}

public function getCurrentTest(): string
{
return $this->getName();
}

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' ) );
}
}

Once this code is added then you are free to create your fixture inside the Fixtures folder and use them within your tests.

Conclusion

Now that your environment for integration tests is setup it is now time to write your first integration test.

If you have no idea how to make it and you are in Portugal maybe you might be interested about my talk on integration testing on the 18 May 2024 at the Porto WordCamp.

However, if it is not the case don’t worry I plan on writing more articles on integration tests and a book on plugin development in the future.

Leave a Reply

Your email address will not be published. Required fields are marked *