Writing Automated Tests in Drupal 8, Part 4: Kernel tests

 

In the last part, we wrote a unit test. Unit tests are really, really fast, but they achieve that speed by not supporting any Drupal functionality at all. Unit tests are great for testing things like individual functions, class methods, or even whole classes such as one that provides static constants and related utility functions. 

So is that it, then? We either bootstrap all of Drupal to run functional tests, or we constrain ourselves to testing pieces that do not rely on Drupal at all? Fortunately for us, there is a middle ground.

Kernel tests sit in between Functional and Unit tests. They do not guarantee a working Drupal UI, but they do allow us to bootstrap selected portions of Drupal in order to run our test. For this reason, kernel tests are often best for testing the programmatic APIs of your module. If it relies on Drupal, but you're not testing the UI, you want a kernel test.

In many ways, writing a kernel test is little different than writing a functional test. Instead of acting as an end-user, it's easier to imagine yourself as a developer hoping to use your module programmatically. This could be as simple as implementing a hook, or as complex as working with the entity system.

For this tutorial, we're going to test a common feature of Drupal 8 modules -- a service class.

<?php

namespace Drupal\my_module;

interface MyServiceInterface {

  /**
   * Gets the label.
   *
   * @param $key
   *   The machine name for the label/message pair.
   *
   * @return string|bool
   *   The human readable label if found, FALSE otherwise.
   */
  public function getLabel($key);

/**
   * Gets the message.
   *
   * @param $key
   *   The machine name for the label/message pair.
   *
   * @return string|bool
   *   The message if found, FALSE otherwise.
   */
  public function getMessage($key);
}

The MyService class provides two methods, ::getLabel() and ::getMessage(). Both expect a single parameter, a string containing the machine name for a label/message pair. As you would expect, the former returns a short, human-readable string for use in titles and headers. The latter returns a longer, human readable message with additional details. 

MyService doesn't internally generate the labels or messages. Instead, it gets them from other parts of our own module, or other modules that provide the label/message pairs. While we would rely on one of those for our test, it's better to provide our own, test-specific version instead. 

The my_module_service_test submodule provides a new label/message pair. This pair is constant and always returns the same values, ensuring that label and message are always consistent. There's nothing special about this submodule other than two important details:

  • It is saved to the tests/modules/ directory of our own module.
  • In the submodule's info file, it is listed as being in the Testing package.

Combined, it instructs Drupal to ignore the submodule. It will not appear in Admin > Extend, you cannot enable it using Drush, but you can enable it in tests.

Now that we know what were testing and how we're going to test it, we can start scaffolding out the test class. Kernel tests are saved to the tests/src/Kernel/ directory of our module. Since we're testing the MyService class, let's name our test MyServiceTest:

modules/custom/my_module
 ├── my_module.info.yml
 ├── my_module.module
 ├── src/
 └── tests/ 
      ├── modules/
      └── src/
           ├── Functional/
           ├── Kernel/
           │   └── MyServiceTest.php
           └── Unit/

Then we can populate it with the class stub. Kernel tests are structured much like other test types we've covered in this tutorial. Let's stub out the test:

<?php

namespace Drupal\Tests\my_module\Kernel;

use Drupal\KernelTests\KernelTestBase;

/**
 * Tests the MyService
 *
 * @group my_module
 */
class MyServiceTest extends KernelTestBase {

  /**
   * The service under test.
   *
   * @var \Drupal\my_module\MyServiceInterface
   */
  protected $myService;

  /**
   * The modules to load to run the test.
   *
   * @var array
   */
  public static $modules = [
    'my_module',
    'my_module_service_test',
  ];

  /**
   * {@inheritdoc}
   */
  protected function setUp() {
    parent::setUp();

    $this->installConfig(['my_module']);

    $this->myService = \Drupal::service('my_module.status_text');
  }
}

Like unit tests and functional tests before it, kernel tests derive from a test-type specific class provided by Drupal. For kernel tests, it's KernelTestBase. As you would expect, this does all the underlying set-up necessary for our kernel test to run.

Our MyServiceTest class looks a bit more like a functional test rather than a unit test. Like a functional test, we have both a $modules static class variable and a ::setUp() method. The $modules variable contains an array of class names for the test framework to enable prior to running our test. Since we're testing my_module, and we need the my_module_service_test submodule we created earlier, we add both to the $modules array.

Our ::setUp() method does a few interesting things. At the bottom, you can see that it gets the service under test and assigns it to the $myService class variable. We'll use that later when we write our test cases. What is strange, however, is this call to ::installConfig()

When we wrote a functional test earlier, it was enough that we listed the module name in the $modules static class variable. This alone is not enough for a kernel test. A kernel test operates in a minimal -- or rather, partial -- Drupal environment. The very core of Drupal is all that's enabled. This grants us the ability to use much of the Drupal API, use services, call hooks, and so on.

The partial Drupal environment also assumes that if you need to enable a module and use some of its features, you will do all the necessary set up. Why? Speed. The test framework does not want to assume your test's needs, and rightfully expects you to spell them out instead. 

Our module, my_module, uses simple config to store critical module settings. Our module provides a default configuration out of the box as a *.yml file in install/config/. In order for our test to work correctly, we need to install that configuration. This is where we get the call to ::installConfig().

The KernelTestBase class provides several of these installation methods:

  •  ::installConfig() for default configuration
  •  ::installSchema() for database tables described by hook_schema()
  • And ::installEntitySchema() for entities

Knowing when to call each requires detailed knowledge of the module you're testing, how it works, and how it stores its data. Often, I forget this step when writing my tests and only discover it after the test fails during it's first run. This can often be an effective strategy as it requires you to be as minimal as possible when writing your test.

Unsurprisingly, each class you create which derives from KernelTestBase is considered a test suite. Individual test cases are written as methods that start with "test". We'll start with two methods; one to test fetching the label, and the other to test fetching the message:

  public function testLabel() {
    $label = $this->myService->getLabel('my_service_test');
    $this->assertTrue($label == 'My Test Label', $label);
  }

  public function testMessage() {
    $message = $this->myService->getMessage('my_service_test');
    $this->assertTrue($message == 'My test message', $message);
  }

Like our other tests, the base class provides a number of ::assert*() methods to communicate to the test framework whether a test case succeeds or fails. Since we're only testing strings, we'll use ::assertEquals() and compare the value returned by the service with what we expect. 

But wait... Aren't we forgetting something?

Negative testing! The methods of the service under test can also return FALSE, if the $key they are provided doesn't match anything. We need to test that too, so let's add one more test method:

  public function testNotFound() {
    $label = $this->myService->getLabel('doesnt_exist_key');
    $this->assertFalse($label);

    $message = $this->myService->getMessage('doesnt_exist_key');
    $this->assertFalse($message);
  }

The above passes a $key that shouldn't ever be used in our module. As a result, both MyServiceInterface::getLabel() and MyServiceInterface::getMessage() should return FALSE. Thus, we'll check the return values with ::assertFalse().

With the test written, we can run it along with the other tests in our module:

$ ../vendor/bin/phpunit --group=my_module
PHPUnit 4.8.36 by Sebastian Bergmann and contributors.

Testing 
...........

Time: 1.22 minutes, Memory: 236.00MB

OK (11 tests, 59 assertions)

The phpunit command is easy to invoke, but....the output is a bit spare. For this reason, I often prefer to run my tests using the same method that Drupal.org does for it's automated tests. Included with each installation of Drupal is the run-tests.sh script inside the core/scripts/ directory. This script is capable of running all the tests for a given module. The output is a bit nicer as well. Drupal.org has an excellent documentary page for using run-tests.sh,

We don't need to change our phpunit.xml in order to use run-tests.sh. Instead, we only need to pass some key pieces of information such as the URL to our local development environment and our database URL. Instead of using group names, we can simply specify the name of the module with --module. I like to also run the script with the --verbose and --color options. This prints out a lot more information, including details about each test. 

$ cd path/to/drupal

$ php core/scripts/run-tests.sh \
      --url http://docker.test \
      --dburl mysql://db_user:db_password@database_server_name/my_db_name \
      --module my_module \
      --verbose \
      --color

Drupal test run
---------------

Tests to be run:
  - Drupal\Tests\my_module\Functional\SettingsPageTest
  - Drupal\Tests\my_module\Kernel\MyServiceTest
  - Drupal\Tests\my_module\Unit\MyStatusTest

Test run started:
  Monday, February 11, 2019 - 04:02

Test summary
------------

Drupal\Tests\my_module\Functional\SettingsPageTest      3 passes                                      
Drupal\Tests\my_module\Kernel\MyServiceTest             3 passes                                      
Drupal\Tests\my_module\Unit\MyStatusTest                2 passes                                      

Test run duration: 1 min 8 sec

Detailed test results
---------------------


---- Drupal\Tests\my_module\Functional\SettingsPageTest ----


Status    Group      Filename          Line Function                            
--------------------------------------------------------------------------------
Pass      Other      SettingsPageTest.   52 Drupal\Tests\my_module\Functional
    
Pass      Other      SettingsPageTest.   84 Drupal\Tests\my_module\Functional
    
Pass      Other      SettingsPageTest.  119 Drupal\Tests\my_module\Functional
    


---- Drupal\Tests\my_module\Kernel\MyServiceTest ----


Status    Group      Filename          Line Function                            
--------------------------------------------------------------------------------
Pass      Other      MyServiceTest       48 Drupal\Tests\my_module\Kernel\Fin
    
Pass      Other      MyServiceTest       59 Drupal\Tests\my_module\Kernel\Fin
    
Pass      Other      MyServiceTest       74 Drupal\Tests\my_module\Kernel\Fin
    

---- Drupal\Tests\my_module\Unit\MyStatusTest ----


Status    Group      Filename          Line Function                            
--------------------------------------------------------------------------------
Pass      Other      MyStatusTest   30 Drupal\Tests\my_module\Unit\Findi
    
Pass      Other      MyStatusTest   49 Drupal\Tests\my_module\Unit\Findi

$

Kernel tests sit in a middle ground between functional and unit tests. They are excellent for when you need to leverage Drupal features programmatically in your test, but you do not require a UI to test. While they are faster to execute than functional tests, they require a more careful approach during the writing process as you need to enable only the Drupal functionality you require to complete the test.

While Kernel tests are still new to me, I can see why they are often the first choice in test types in Drupal 8. In a properly designed module, testing the API of your module should take precedence over any interaction with the UI. Once your module's APIs have been tested, only spare number of functional tests need be written to cover remaining interaction. 

Next time we'll explore Drupal's newest testing framework when when use Nightwatch.js to test Javascript.

This post was created with the support of my wonderful supporters on Patreon.

If you like this post, consider becoming a supporter at:

Thank you!!!