Skip to main content

Kernel testing custom modules in Drupal 8

Back-end Development
Drupal
Planning & Strategy

The usefulness of automated testing varies greatly in the world of software development. We know it makes sense for libraries that are relied upon by a community, but it's difficult to rationalize the time spent on developing tests for one-off custom Drupal modules. 

Some reasons for that are:

  • Code dependence on a large framework.
  • Processor time required to build a site and database for each individual test.
  • Development time required that can increase project costs.
  • Risk of code changes and frequency is low. Meaning–you may not need to change code often, and if something goes wrong, the impact is small, and low stakes.

But this doesn't mean there are not one-off projects that can benefit from some automated testing. One such example might be something I found myself developing for a recent project: A module that introduces functionality that uses an external authentication system for logging in users. 

Reasons FOR developing automated tests for this module include:

  • High risk relating to customer's inability to log in if something goes wrong.
  • Dependency on a bespoke external system that we have little control over.
  • A wide array of expected outcomes based on the external response, and what information is stored locally.
  • Manual testing the login process for a variety of scenarios is tedious.

Unless you have experience developing contributed code where testing is required, there can be a lot of friction to getting tests running for a custom module that has a lot of dependencies we have always had the pleasure of assuming just work. Online resources for testing unique custom modules are few and far between, and what does exist is often contrived. In this article, I'll show how I set up a test class for a project's external authentication module and discuss the pitfalls along the way.

Determining which types of tests to use

There are three main types of tests in Drupal 8: Unit, Kernel, and Functional (of which has a few different types).  You can learn more about the differences here.  But for my project, Kernel tests hit the sweet spot. To be honest, I strive for the simplest tests with the least number of dependencies. I would have loved to used simple Unit tests, but any module that interacts with Drupal in a meaningful way is hard to do this with.  Kernel tests allow us to define the specific resources we need from Drupal, and nothing more. They are not fast like Unit tests, but are not anywhere near as heavy as most Functional tests.

Setup

Unlike some of the specifics when testing a custom module, there are plenty of resources for getting tests (PHPUnit) running.  So I won't cover that again. 

Testing Drupal 8 documentation

A few additional notes or tips about setting things up. 

  • You can put the phpunit.xml file any number of places. The site root, the site/default directory, etc. But know that when running PHPUnit itself, it expects the phpunit.xml file to be in the same directory that the command is issued. So if you have it somewhere else (like /sites/default...)  You will want to use the --configuration option. Here is an example assuming you are running the command from the "Drupal Root".
vendor/bin/phpunit --configuration web/sites/default
  • You can specify a specific test file, a specific directory, or a test group as the final argument when running PHPUnit. This example specifies a file. The test group is specified via annotation in the test class comment docs.
vendor/bin/phpunit --configuration web/sites/default  web/modules/contrib/token/tests/src/Kernel/ArrayTest.php
  • You can run tests with the Drupal specific run_tests.sh script in addition to calling PHPUnit directly.  In many ways, the output is nicer, especially if you are running lots of tests.  But when developing the tests themselves I prefer PHPUnit directly as it shows more information on what went wrong when a test fails, like exceptions or syntax errors.
  • You should leverage environment variables when running tests.  When we run tests we actually create a script that will set these as necessary and then run the command like so...
source setup-testing.sh && php web/core/scripts/run-tests.sh --url $SIMPLETEST_BASE_URL --dburl $SIMPLETEST_DB --sqlite sites/default/files/db.sqlite [testing_example]
  • Notice in the command above, I specify a --sqlite option.  Another nice thing about run-tests.sh is that it is equipped to run test from SQLite, instead of MySQL. This is faster and requires less resources than creating a MySQL schema, etc.  And the code won't know the difference.

Building the Test Class

Let's assume we are going to use the following command for running tests (example taken from the token module). 

vendor/bin/phpunit --configuration web/sites/default  web/modules/contrib/token/tests/src/Kernel/ArrayTest.php

So in our custom module, let's create a new file at the following path:

mymodule/tests/src/Kernel/NetworkTest.php

Let's start with something like this in our new test class. 

<?php

namespace Drupal\Tests\mymodule\Kernel;

use Drupal\KernelTests\KernelTestBase;

/**
 * Test description.
 *
 * @group mymodule
 */
class NetworkTest extends KernelTestBase {

  /**
   * {@inheritdoc}
   */
  public static $modules = [];

  /**
   * {@inheritdoc}
   */
  protected function setUp() {
    parent::setUp();
    // Mock required services here.
  }

  /**
   * Test callback.
   */
  public function testNetwork() {
    $test_variable = "Not Empty";
    $this->assertNotEmpty($test_variable, "This variable shouldn't be empty.");
  }

}

Super contrived!  But we have to start somewhere, and it's better to get the kinks out now before adding more complexity, and wondering whether what you started with was even good at all. Let's run it.

vendor/bin/phpunit --configuration web/sites/default web/modules/custom/mymodule/tests/src/Kernel/NetworkTest.php

Hopefully it worked, if not, better head over to the troubleshooting article and get things sorted out.

Setting up Schemas

As a general principle, tests should include(run) the code needed to execute your test, and nothing else. It can be frustrating when writing tests that the test cannot be simply written the way we do in a custom module or theme, but it makes sense when you think about it. A new database is built for every test you create, this takes assumptions out of the equation and starts with a clean slate each execution. It is also what makes a good test repeatable. The way we tell our test what to include is by installing schemas. Think of this as your database tables.

In the `setUp()` method there are a few ways we can inform our test about the required schemas. One way is simply by using the name of the schema(table) and the module responsible for installing it.

$this->installSchema('user', ['users_data']);

The other way, is a bit of a shortcut for installing schema required for specific entities. 

$this->installEntitySchema('user');

It can be a little bit of a challenge to determine what schemas your code requires, especially when it relates to underlying Drupal functionality that you may not even be aware of. My best advice is to try and include what you think will be needed and run the test expecting the test to fail. The error displayed will show you what is missing. See the troubleshooting article for more details about deciphering the error messages to reveal what schemas are required.

Testing your code

If you have gotten this far, and successfully run a test, you are ready to actually add the code needed to test your custom service. Tests are generally created on a method by method basis, but there isn't any hard rules.  You may discover at this point that the way you have written your service isn't conducive to testing.  You may find that your method makes too many assumptions, or doesn't isolate functionality well.  Visit our article on writing testable services for information about separating logic in a way that makes testing easier.

Here is an example of some authentication test code of which you can learn more about in our network service testing article.

/**
 * Tests logging into an external environment.
 */
public function testSuccessfulLogin() {
  $uid = $this->getNewAuthService()->processLogin('mbopp@rapiddg.com', 'test');
  $this->assertNotEmpty($uid);

  $user = $this->userStorage->load($uid);
  $this->assertNotEmpty($user);
  $profile = $this->profileStorage->loadByUser($user, 'crm');
  $this->assertNotEmpty($profile);
  $this->assertEquals('7b2ff57a-b64e-4c9c-8843-18df60ca5a16', $profile->field_crm_id->getString());

  $uid2 = $this->getNewAuthService()->processLogin('mbopp@rapiddg.com', 'test');
  $this->assertNotEmpty($uid2);
  $this->assertEqual($uid, $uid2);
  $this->expectExceptionCode(AuthorizationService::AUTHENTICATION_FAILURE_CREDENTIALS);
  $this->getNewAuthService()->processLogin('mbopp@rapiddg.com', 'badpass');

  $this->expectExceptionCode(AuthorizationService::AUTHENTICATION_FAILURE_FIND);
  $this->getNewAuthService()->processLogin('mbopp@not.com', 'test');
}

 

How much code to test in each test method

It may seem cleanest at first to separate your tests into many methods, each testing another aspect of your code.  This makes sense when creating traditional unit tests, but there is a downside to do this in Drupal kernel tests. 

Kernel tests take a long time to set up. As was previously mentioned, schemas need to be installed, and contributed Drupal code needs to be bootstrapped.  This happens for each test method, and then the environment is completely destroyed.  Because of this, it makes sense to group as much as possible into each test to maximize efficiency.  For example, if testing authentication logic, consider putting all scenarios (successful login, bad password, username taken, etc) in the same test method.