Let’s say we want to develop a Drupal site, and we chose a commonly used custom profile approach, all things that are needed by the site are stored in a profile: configs, modules, themes.

Site structure

This will allow us to run a drush command to get the site up and running:

drush site-install our_profile_name

Also, we want our site to be easy to setup/run on different machines that’s why we want to use Docker for this purpose. And of course, we want to test the site functionality, to cover it with unit/functional tests.

Project setup

We will use Drupal Composer template to download Drupal and all needed dependencies. You just need to run a single composer command:

composer create-project drupal-composer/drupal-project:8.x-dev drupalsite --no-interaction

Drupal code will be downloaded to drupalsite/web folder, Drupal dependencies — to the drupalsite/vendor folder.

The next step is to setup Docker containers. I recommend to use Docker4Drupal for local Drupal setup, but we will go with a custom docker-compose file with a minimal configuration in this tutorial.

Minimal docker-compose configuration for Drupal site

We will use just 3 containers for now:

mariadb as our database.

as our database. php as our PHP interpreter with PHP-FMP service, drush, and Drupal console.

as our PHP interpreter with PHP-FMP service, drush, and Drupal console. nginx as our server which uses PHP-FPM as a backend. Port 80 is mapped from the container to the host machine in order to see a Drupal site by visiting localhost.

Now we can run docker-compose up and should access drupal installation page by going to localhost.

Custom profile

I already created a custom profile called Urban Profile(the whole project is available here) with next functionality:

Urban definition content type with Title, Definition, Example fields.

Urban definition node creation page

Title field is required in a content type. When you enter a Title and click “Save” button Definition and Example fields will be auto-populated with values from API call to urbanscraper.herokuapp.com.

Urban definition list block is added to the block layout.

This block lists all added urban definitions. When you click on the item in the list it’s definition will be loaded by AJAX.

Custom block “Urban definition list”

In order to install the profile you need to:

Clone this repository.

Run composer install from “drupalsite” directory.

from “drupalsite” directory. Run docker-compose up from “drupalsite” directory.

from “drupalsite” directory. Go to localhost (Drupal installation page will be opened), select “Urban profile” as a profile to install and then follow remaining steps.

The code structure of a profile:

UrbanDefinition

Value object class to represent a response from urbanscraper.herokuapp.com API.

Value object class to represent a response from urbanscraper.herokuapp.com API. UrbanDictionaryServiceInfterface

Interface to interact with API.

Interface to interact with API. UrbanDictionaryService

Implementation of UrbanDictionaryServiceInfterface, uses Guzzle HTTP client to send requests to API.

Implementation of UrbanDictionaryServiceInfterface, uses Guzzle HTTP client to send requests to API. UrbanDefinitionListBlock

Block plugin to output a list of all urban terms.

Block plugin to output a list of all urban terms. UrbanDefinitionAjaxController

Ajax controller used by block to load definitions when a user clicks on the urban definition term.

Ajax controller used by block to load definitions when a user clicks on the urban definition term. urban_module_node_presave() hook

Used to auto-populate Definition and Example fields on node save.

Write a Unit test

UrbanDefinitionService is a great candidate for writing a unit test. This is an independent unit/service that provides functionality by itself. A bad example of a unit test would be some class that has plenty of external dependencies or is too abstract. In most cases, you can cover the functionality of those classes by functional tests.

But back to our unit test. Drupal uses phpunit which is great and it provides an own configuration file that should be used by PHPUnit (web/core/phpunit.xml.dist). It’s better to clone this file and keep our modifications to it under version control. You can find a modified version in “drupalsite/web” folder.

As our PHP interpreter is inside a Docker container the proper command to execute a unit test would be

docker exec drupal_testing_php vendor/bin/phpunit

--configuration=web PATH_TO_TEST_FILE

Let’s take a look at the only one method of the UrbanDictionaryService class:

We have a dependency on the HTTP client and Logger services. In order to write a unit test for the method above we will need to mock these services and test 2 scenarios:

When API returns a response with a correct JSON

When there is an error during HTTP request to the API

Our test class should extend Drupal\Tests\UnitTestCase class, should have Drupal\tests\modulename\Unit namespace, and be located inside the tests/src/Unit directory of our module. If you ask why I need to put test exactly in that folder you can check <testsuites> section inside phpunit.xml.dist.

And now let’s implement our unit test. Here is the code:

Mock creation is done with the help of ::createMock method which returns a mock object. Then we can describe it’s behavior.

method which returns a mock object. Then we can describe it’s behavior. After mocks are created we can create our service instance passing mocks to the constructor.

And finally, we can execute methods of the service and do test assertions.

Nothing complicated, the same as ordinary PHPUnit test except the additional rules on test class placement in the filesystem and namespace naming.

Now, you can run your test:

docker exec drupal_testing_php vendor/bin/phpunit — configuration=web web/profiles/custom/urban_profile/modules/urban_module/tests/src/Unit/UrbanDictionaryServiceTest.php

Write a Kernel test

The next step will be to test our urban_module_node_presave() hook.

This hook is executed right before the node object is saved. We check the bundle of the node and if it’s an urban_definition node we call an UrbanDefinitionService service to populate field values.

We have 2 options here:

Write a Functional test. It will run the full Drupal site installation.

Write a Kernel test. It is like a functional test, but Drupal runs in a minimal mocked environment(virtual file system, modules are loaded but they do not run the installation process, database tables needs to be created on our behalf). This kind of tests can be created to test non-UI stuff.

Obviously, we will choose a Kernel test. The only trouble is that we need to manually create tables and an urban definition content type with corresponding fields. Also would be nice to mock urban_module.service service to be not dependent on real API (as we want to test only the hook).

Let’s take a look at the test:

$modules static variable declares which modules should be enabled during the test run.

static variable declares which modules should be enabled during the test run. ::installEntitySchema call will create needed tables for node and user tables (node module is dependent on user module).

call will create needed tables for node and user tables (node module is dependent on user module). Then we manually create a content type and adding fields to it.

Also, we replace urban_module.service service with a custom mock service. This involves 2 steps. First, we want to keep our service mock inside a directory with our test, thus we need to make a directory a part of autoloader ( ::addPsr4 call). Secondly, we need to swap the service inside the container. Fortunately, KernelTestBase implements ServiceProviderInterface . That means we can override ::register method in order to modify the container.

Actually, there is another way to swap a service. You can create a testing.services.yml file in sites/default folder. Kernel test will automatically pick up definitions from that file. But I don’t like this approach as this file is outside of tests codebase.

service with a custom mock service. This involves 2 steps. First, we want to keep our service mock inside a directory with our test, thus we need to make a directory a part of autoloader ( call). Secondly, we need to swap the service inside the container. Fortunately, implements . That means we can override method in order to modify the container. Actually, there is another way to swap a service. You can create a file in folder. Kernel test will automatically pick up definitions from that file. But I don’t like this approach as this file is outside of tests codebase. Our test method ::testPresaveHook is pretty simple: create a node, save it, test field values.

is pretty simple: create a node, save it, test field values. For kernel tests, you need a working database connection. phpunit.xml.dist file contains a SIMPLETEST_DB variable. In order to run a kernel test, you need to provide the correct value —

<env name=”SIMPLETEST_DB” value=”mysql://drupal:drupal@mariadb/drupal”/>

Write a Browser test

And finally functional tests. These are the most slow-running tests because they run a full site installation during the set-up phase. But as a benefit, we are able to test UI features.

All functional tests extend BrowserTestBase but there are 2 options:

To directly extend BrowserTestBase and then a headless Guotte browser emulator will be used.

To extend WebDriverTestBase and then use a browser emulator with Javascript support.

In both cases, we need to update SIMPLETEST_BASE_URL variable in phpunit.xml.dist.

<env name=”SIMPLETEST_BASE_URL” value=”http://nginx"/>

The good functionality to cover with a Functional test will be our custom block:

Let’s first test that if we add a number of urban definitions nodes to the site the frontpage will display all added node titles in the block.

For functional tests, we need to extend Drupal\Tests\BrowserTestBase class and put the file into Functional directory.

$profile static variable declares which profile we want to install. Providing urban_profile as a value will save us from creating/adding all configs/modules manually.

static variable declares which profile we want to install. Providing urban_profile as a value will save us from creating/adding all configs/modules manually. ::createNode method allows us to create nodes easily.

method allows us to create nodes easily. ::drupalGet allows to fetch the web page. In order to run assertions on page content it’s good (but not required) to get an assertSession . The object we get after calling $this->assertSession() provides a lot of useful page content assertion methods.

Write a WebDriver test

The only thing that we can’t test with a previous test is Javascript interactions.

So let’s create another very similar test but which will also to test Javascript interactions.

First, we need to install Selenium webdriver. As we use docker we can add a selenium container (Selenium with a chrome browser) to the docker-compose file.

Then we need to update phpunit.xml.dist. As our selenium endpoint is located at http://chrome_browser:4444/wd/hub we need to set MINK_DRIVER_ARGS_WEBDRIVER variable

<env name="MINK_DRIVER_ARGS_WEBDRIVER" value='["chrome", null, "http://chrome_browser:4444/wd/hub"]'/>

The test itself should be located inside “FunctionalJavascript” folder.

There are not a lot of differences with the previous test. We use the same API, same assertions, the only difference is that JS is actually executed in Chrome browser during test run.

::assertWaitOnAjaxRequest waits for AJAX request to be completed.

Conclusion

Drupal 8 provides instruments/API to write unit, kernel, functional tests in a more or less unified way. It’s easy to start writing tests for Drupal modules if you are already familiar with phpunit. Documentation is also good, I advise you to check it. Also, more advanced information is available in this video.