Skip to content
Cyrille C edited this page Mar 8, 2023 · 19 revisions

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.

Creating a simple unit test

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.

Creating the class

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 be testReturnAsExpected .
namespace RocketLauncher\Tests\Unit\inc\Engine\MyNamespace\MyClass;

use RocketLauncher\Tests\Unit\TestCase;

class Test_MyMethod extends TestCase {

   public function testReturnAsExpected() {

   }

}

Mocking classes and functions

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.

Mock a class

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

Mock a function

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.

Using Fixtures

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.

Scenarios

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 Unit 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,
     ]
   ],
];

Use your scenarios in a test

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) {

   }

}

Generating the class and fixtures with the CLI

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.

Creating an Integration test

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.

Setup the integration tests environment

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.

Creating an simple integration test

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 be testReturnAsExpected .

Launching integration tests

To launch integration tests you can use the command composer run test-integration.

Using fixtures

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) {

   }

}

Isolating an action or a filter

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 and RocketLauncher\Tests\Integration\FilterTrait for a filter.
  • Create a set_up method and use the method unregisterAllCallbacksFromActionExcept for an action and unregisterAllCallbacksFromFilterExcept for a filter to disable all callbacks before the test.
  • Create a tear_down method and use the method restoreWpAction for an action and restoreWpFilter 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');
   }

}

Overriding a filter

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

Generating the class and fixtures with the CLI

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.

Creating an external run group for my Integration tests

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.

Adding a group to a class

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

Creating a special run in the composer.json

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"
    ],

Loading specific resources depending on the 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';
    }
);

Generating the external run group with the CLI

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.