-
Notifications
You must be signed in to change notification settings - Fork 0
Tests
This boilerplate offers an architecture that makes testing your code simple and time efficient.
For every test you will be able to fast generate new test cases with the fixtures. You will be also able to mock unit tests quickly using Mockery and Brain Monkey. Finally, the subscriber architecture will make it easier for you to create integration tests for your code.
Concerning tests themselves, tests for the boilerplate are divided in 3 parts:
- Fixtures: Theses are assets and scenarios for your tests.
- Unit tests: Theses tests are here to make sure a class is working properly.
- Integration tests: Theses tests are here to make sure the plugin is working properly when loaded.
First we we will how to create a simple testing class testing a method.
For that first we will have to create a file from the name of the method inside the tests/Unit/inc
folder and following the namespace from the class the method belongs to.
Example if we want to test the method my_method
from the class RocketLauncher\Engine\MyNamespace\MyClass
, we will create the file myMethod.php
in the folder tests/Unit/inc/Engine/MyNamespace/MyClass
.
Inside that class we will have then to add the following content:
- The namespace from your test that follows the path from your class in our example it is
RocketLauncher\Tests\Unit\inc\Engine\MyNamespace\MyClass
. - The definition from the class with the following name Test_ followed by the name of the method, for your example will be
Test_MyMethod
. - That class should be extending
RocketLauncher\Tests\Unit\TestCase
. - Finally that class should contain a public method starting by
test
and that describe the usage from the test in your case it will betestReturnAsExpected
.
namespace RocketLauncher\Tests\Unit\inc\Engine\MyNamespace\MyClass;
use RocketLauncher\Tests\Unit\TestCase;
class Test_MyMethod extends TestCase {
public function testReturnAsExpected() {
}
}
Now that we know how to create a class we will now learn how to deal with a class using other or with a class using some external function from WordPress for example.
Let’s imagine the class we want to test has the following content:
namespace RocketLauncher\Engine\MyNamespace\MyClass;
class {
protected $dependency;
public function __construct(\MyDependency $dependency) {
$this->dependency = $dependency;
}
public function my_method() {
$id = $this->dependency->method(12);
$post = get_post($id);
}
}
We will in this case to mock two things:
- The class
MyDependency
that our class use to use its interface without testing its content. - The method
get_post
that my_method use to prevent use from re-implementing the method.
In this boilerplate we use Mockery to mock classes.
To mock a class with Mockery we use the Mockery::mock method this way:
$mock = Mockery::mock(MyDependency::class);
Once we got the mock object we can then set expectation this way:
$mock->expects()->method(12)->andReturn(45);
For more information on how Mockery work you can check their documentation
To mock method in Rocker launcher we are using Brain Monkey.
Brain Monkey allows us to mock function with an interface close to Mockery.
To mock a function with Brain Monkey we can use the expects function this way:
use Brain\Monkey\Functions;
Functions\expect('get_post')->with(48)->once()->andReturn(false);
For more information on how Brain Monkey work you can check their documentation.
Now that we know how to create a simple unit test to check the behavior of a class, we will now learn to use fixtures to reduce the number of tests to write.
Often when you write tests you will have to test multiple behavior on the same method increasing rapidly the number of test and the repetition from your code.
That why fixtures are used inside Rocket launcher.
Fixtures allows you to create scenarios with set of values and expectations for each of them allowing a same test to have multiple runs.
For creating a fixture nothing more simple:
if we take back your example with the my method class that had a test at the path tests/Unit/inc/Engine/MyNamespace/MyClass/myMethod.php
then we need to create a fixture following the exact same path at the only difference we are replacing Uni
t by Fixtures
:
`tests/Fixtures/inc/Engine/MyNamespace/MyClass/myMethod.php'
Then inside that file we will create an array with a entry for each scenario:
return [
'myFirstScenario' => [
],
'mySecondScenario' => [
],
];
Finally for each scenario we will create two sets of values:
-
config
: values that we pass to the test to configure itself and initialize it -
expected
: values that we will match against the test to be sure the output from the test is valid.
At the end our fixture file will have the following content:
return [
'myFirstScenario' => [
'config' => [
'my_value' => 12,
],
'expected' => [
'my_expected_value' => new stdClass(),
]
],
'mySecondScenario' => [
'config' => [
'my_value' => 15,
],
'expected' => [
'my_expected_value' => false,
]
],
];
Once the fixture defined the next step is to link it to the actual test.
For that we will have:
- To add a provider on our old test.
- Then replace constants by the new values provided by the provider
To add a provider to your test we will have to use the docblock from the method and add it :
namespace RocketLauncher\Tests\Unit\inc\Engine\MyNamespace\MyClass;
use RocketLauncher\Tests\Unit\TestCase;
class Test_MyMethod extends TestCase {
/**
* @dataProvider providerTestData
*/
public function testReturnAsExpected() {
}
}
Once we done that the data will be automatically loaded from the fixture however theses data won’t be used by our test.
To do so we will have to add two parameters to your method config and expected that will be fed with values present inside of each scenario from our fixture file:
namespace RocketLauncher\Tests\Unit\inc\Engine\MyNamespace\MyClass;
use RocketLauncher\Tests\Unit\TestCase;
class Test_MyMethod extends TestCase {
/**
* @dataProvider providerTestData
*/
public function testReturnAsExpected($config, $expected) {
}
}
Creating a test with fixture can be time consuming that’s why we created a way to generate it rapidly with the CLI.
To generate the code we saw in the previous part, we could have use the command:
bin/builder test RocketLauncher/Engine/MyNamespace/MyClass::my_method --type unit --scenarios myFirstScenario,mySecondScenario
To know more about the CLI, you can check your documentation page about it.
Integration tests are here to check how classes are working together.
For theses tests we will have to load a WordPress instance first and include the plugin after it loaded and that why we will have to do some setup before playing with our tests.
To run your integration test we will first need a SQL database for your WordPress instance.
Once it is setup we will launch the following command:
bin/install-wp-tests.sh DATABASE_NAME DATABASE_USER DATABASE_PASSWORD DATABASE_HOST
That will download an install a testing version of WordPress inside your /tmp
folder that will be use later in the integration tests.
Inside that class we will have then to add the following content:
- The namespace from your test that follows the path from your class in our example it is
RocketLauncher\Tests\Unit\inc\Engine\MyNamespace\MyClass
. - The definition from the class with the following name Test_ followed by the name of the method, for your example will be
Test_MyMethod
. - That class should be extending
RocketLauncher\Tests\Integration\TestCase
. - Finally that class should contain a public method starting by
test
and that describe the usage from the test in your case it will betestReturnAsExpected
.
To launch integration tests you can use the command composer run test-integration
.
Once the fixture defined the next step is to link it to the actual test.
For that we will have:
- To add a provider on our old test.
- Then replace constants by the new values provided by the provider
To add a provider to your test we will have to use the docblock from the method and add it :
namespace RocketLauncher\Tests\Integration\inc\Engine\MyNamespace\MyClass;
use RocketLauncher\Tests\Integration\TestCase;
class Test_MyMethod extends TestCase {
/**
* @dataProvider providerTestData
*/
public function testReturnAsExpected() {
}
}
Once we done that the data will be automatically loaded from the fixture however theses data won’t be used by our test.
To do so we will have to add two parameters to your method config and expected that will be fed with values present inside of each scenario from our fixture file:
namespace RocketLauncher\Tests\Integration\inc\Engine\MyNamespace\MyClass;
use RocketLauncher\Tests\Integration\TestCase;
class Test_MyMethod extends TestCase {
/**
* @dataProvider providerTestData
*/
public function testReturnAsExpected($config, $expected) {
}
}
Often actions and filters registered in a plugin is also used by other part of WordPress code and that can lead to some complexity in handling the output from integration tests.
That's why in Rocket launcher, you have a trait to unregister all callback except your on the action during the test.
To disable all callback except yours you need:
- First to use the trait
RocketLauncher\Tests\Integration\ActionTrait
for an action andRocketLauncher\Tests\Integration\FilterTrait
for a filter. - Create a
set_up
method and use the methodunregisterAllCallbacksFromActionExcept
for an action andunregisterAllCallbacksFromFilterExcept
for a filter to disable all callbacks before the test. - Create a
tear_down
method and use the methodrestoreWpAction
for an action andrestoreWpFilter
for a filter to reset callbacks on that action/filter.
namespace RocketLauncher\Tests\Integration\inc\Engine\MyNamespace\MyClass;
use RocketLauncher\Tests\Integration\TestCase;
use RocketLauncher\Tests\Integration\ActionTrait;
class Test_MyMethod extends TestCase {
use ActionTrait;
public function set_up() {
parent::set_up();
$this->unregisterAllCallbacksFromActionExcept('my_action', 'my_method');
}
public function tear_down() {
$this->restoreWpAction('my_action');
parent::tear_down();
}
/**
* @dataProvider providerTestData
*/
public function testReturnAsExpected($config, $expected) {
do_action('my_action');
}
}
Another way to manipulate the behavior from the application during an integration test is to override some filters to change their output.
To do that we proceed in 3 steps:
- We create a callback on the test class returning the value we want.
- We create a
set_up
method and register the filter and your callback inside it. - We create a
tear_down
and unregister the callback from the filter.
This way we have the test returning the value we want during the test without impacting other tests.
namespace RocketLauncher\Tests\Integration\inc\Engine\MyNamespace\MyClass;
use RocketLauncher\Tests\Integration\TestCase;
class Test_MyMethod extends TestCase {
protected $configs;
public function set_up() {
parent::set_up();
add_filter('my_filter', [$this, 'my_callback']);
}
public function tear_down() {
remove_filter('my_filter', [$this, 'my_callback']);
parent::tear_down();
}
/**
* @dataProvider providerTestData
*/
public function testReturnAsExpected($config, $expected) {
$this->configs = $configs;
}
public function my_callback() {
return $this->configs['override'];
}
}
Creating a test with fixture can be time consuming that’s why we created a way to generate it rapidly with the CLI.
To generate the code we saw in the previous part, we could have use the command:
bin/builder test RocketLauncher/Engine/MyNamespace/MyClass::my_method --type integration --scenarios myFirstScenario,mySecondScenario
To know more about the CLI, you can check your documentation page about it.
Sometimes modifying and isolating actions and filters is not enough to make a test run in the right environment.
A simple example of that is some hosts that have a specific environment and that are detectable only by using a constant.
In that special case it is impossible to keep the tests isolated from the others and we will have to create it a specific run to set its own configuration and not impact any other test.
The first step when creating an external run is to identify classes to run inside that specific run.
For that in Rocket launcher we use the @group
attribute from the docblock that we will add on top of the class:
namespace RocketLauncher\Tests\Integration\inc\Engine\MyNamespace\MyClass;
use RocketLauncher\Tests\Integration\TestCase;
/**
* @group MyGroup
*/
class Test_MyMethod extends TestCase {
/**
* @dataProvider providerTestData
*/
public function testReturnAsExpected($config, $expected) {
$this->configs = $configs;
}
}
Once we created our class group and added it to all classes from your external run, we will now create the external run by itself.
For that we will have modify our composer.json
and more specifically the scripts
part:
- On the
test-integration
script we will have to add our group to the excluded ones to prevent our tests to run on normal run:
"test-integration": "\"vendor/bin/phpunit\" --testsuite integration --colors=always --configuration tests/Integration/phpunit.xml.dist --exclude-group AdminOnly,MyGroup",
- We will add a new script to run our external run with
test-integration-my-group
as key and with the follow content:
"\"vendor/bin/phpunit\" --testsuite integration --colors=always --configuration tests/Integration/phpunit.xml.dist --group MyGroup”
- Finally we will add our new external run to the
run-tests
script to make sure it will run during CI:
"run-tests": [
"@test-unit",
"@test-integration",
"@test-integration-adminonly",
"@test-integration-my-group"
],
The last step is to add the custom values that activates only on the external run.
For that we will have to modify the bootstrap file from the integration tests at the following path /tests/Integration/bootstrap.php
.
Inside the callback from the filter muplugins_loaded
and before require ROCKET_LAUNCHER_PLUGIN_ROOT . '/rocket-launcher.php';
, we need to use isGroup
method from WPMedia\PHPUnit\BootstrapManager
to add your logic:
<?php
namespace RocketLauncher\Tests\Integration;
use WPMedia\PHPUnit\BootstrapManager;
define( 'ROCKET_LAUNCHER_PLUGIN_ROOT', dirname( dirname( __DIR__ ) ) . DIRECTORY_SEPARATOR );
define( 'ROCKET_LAUNCHER_TESTS_FIXTURES_DIR', dirname( __DIR__ ) . '/Fixtures' );
define( 'ROCKET_LAUNCHER_TESTS_DIR', __DIR__ );
define( 'ROCKET_LAUNCHER_IS_TESTING', true );
// Manually load the plugin being tested.
tests_add_filter(
'muplugins_loaded',
function() {
if ( BootstrapManager::isGroup( 'MyGroup' ) ) {
//your custom env
}
// Load the plugin.
require ROCKET_LAUNCHER_PLUGIN_ROOT . '/rocket-launcher.php';
}
);
Creating an external run can be time consuming that’s why we created a way to generate it rapidly with the CLI.
To generate the code we saw in the previous part, we could have use the command:
bin/builder test RocketLauncher/Engine/MyNamespace/MyClass::my_method --type integration --external MyGroup
To know more about the CLI, you can check your documentation page about it.
Rocket launcher