Drupal 8 unit testing with PHPUnit


Error message

Deprecated function: The each() function is deprecated. This message will be suppressed on further calls in _menu_load_objects() (line 569 of /homepages/46/d762693627/htdocs/dc/includes/menu.inc).
Drupal 8 unit testing with PHPUnit


PHPUnit is used in Drupal 8 for unit testing individual class methods without requiring a full Drupal environment. SimpleTest is still supported but should only be used for tests requiring a full Drupal environment.

In order to test Drupal modules without a Drupal environment, you need to use PHPUnit's object mocking functionality to create mock objects and inject these into the objects being tested.

This video (29 mins) is a really good introduction to the key concepts (the actual code examples are slightly out of date though).


At the time of writing, there isn't much documentation about how best to write unit test for D8 and quite a bit of the information out there is contradictory.

The best way to understand how tests work is by looking through the examples in our existing d8 code as well as core are contrib modules.

I found that even for a small number of relatively simple modules, setting up the tests correctly was quite challenging. Each time the method you're testing calls a Drupal function, chances are you need to inject dependent mock objects into the container. Examples of this can now be seen in our code and below.

Dependency Injection via the Services and the Container

Drupal 8 makes heavy use of services, which are pieces of code providing global functionality, such as sending email, logging, etc.

As services are defined as classes, they can be switched interchangeably with similar services implementing the same methods or interface.

This is very useful for unit testing, because the services can be switched out with simple mock objects.

Each class of service is identified by an id string ("string_translation" in the example below). There is a global container object which references the objects currently providing each service.

Example 1

In the example below (from sites/modules/custom/hello_world/tests/src/Unit/HelloControllerTest.php), a stub translation object is added to the container as the "string_translation" service, so that when HelloController->content() calls the Drupal t() function, the t() function has access to the "string_translation" service that it depends on. This allows us to test HelloController→content() without needing a full Drupal environment. Note that the stub translation object is created using the special function getStringTranslationStub().

Example 2

In this example from CustomerAccountsOrderLatestBlockTest.php, mock objects are created for the services "atd_api_client.api_client" and "atd_customer_accounts.atd_customer_accounts". These are registered with the controller.

Then in the same file the method testBuildBlockOrderLatest() uses the mock "atd_customer_accounts.atd_customer_accounts" service when testing the block build method:

Dependency Injection by passing mocks directly to controllers

Classes often receive their dependent objects as constructor parameters, making it easy to inject mocks like this:

Test groups

Unit tests can be arranged into groups. Each test can be in zero or more groups.

Groups are specified in the comments of each unit test's class, like this:

For our ATD tests, we should ensure that the tests are part of both the "atd" group (so we can easily run the tests for all our code) as well as the group named after the module being tested (so we can easily test just that module).

Running unit tests

To run tests, ssh into the server, cd to the "core" directory in the Drupal root and run the following commands:

# Run ALL tests:
# List all test groups:
../vendor/bin/phpunit --list-groups
# Run tests of given group:
../vendor/bin/phpunit atd
../vendor/bin/phpunit atd_customer_accounts
# Run tests with verbose mode enabled - useful if PHPUnit is telling you that your test is "risky" and you want to know why
../vendor/bin/phpunit -v atd


The phpunit command's -v option is useful if you are being told that your test is "risky", as it will give you more information about what exactly is causing that to happen .