Skip to main content
ocean waves

Blog Post

Writing Simple (PHPUnit) Tests for Your D8 module

I've previously explained why automated tests should be written for Drupal. and I've shown how to add tests for a Drupal 7 module, now we'll look at how to add a test for a Drupal 8 module.

Note: In the interest of making this a standalone article, much of the same verbose explanations will be repeated from the previous article.

Our goal: Make sure the module doesn't break the site

This guide is going to do add tests to do two very simple things:

  1. Enable a module.
  2. Make sure the site doesn't blow up.

These are two simple building blocks that can be improved upon over time. Doing such a simple test will also help confirm that there aren't any facepalm-worthy mistakes in the module ... like the bug left in Metatag 7.x-1.15 that blew up any sites which had enabled the Metatag Validation submodule. Ahem. Sorry about that.

Types of tests

Before we begin, it's worth noting that there are several types of tests supported in Drupal 8, two more than Drupal 7 had:

  1. Unit tests are designed for confirming functions and classes, i.e. the module's API, work the way they're supposed to. They don't load the database and so also don't load a full Drupal site, which greatly limits what can be used to run the tests. It's worth noting, however, that they are super fast to run and can complete in mere seconds, so after getting the hang of writing them it can make the testing process a little less time intensive. Tests of this type are built using the UnitTestCase class.
  2. Functional tests load a full installation of Drupal to be toyed with, so they're ideal for confirming the module's functionality. These tests are built from the BrowserTestBase class. The idea is to script a pretend web browser to step through the website and check what happens on the site - look for form fields, look for text that should be there, submit forms, make sure that the site doesn't blow up, etc. These tests are the easier of the two to write because it literally involves stepping through the website like a person would normally. A major downside, however, is that they are slower than unit tests - because it loads a full Drupal installation before beginning each set of tests it can take a few minutes to run a single test. That said, it's totally worth it.
  3. Kernel tests, built from KernelTestBase, provide a middle ground between raw unit tests and the more verbose functional tests. Many functional tests will be able to be converted to kernel tests, and as they run faster it'd be worth taking the time to do so.
  4. JavaScript tests, built with JavaScriptTestBase, go further than the functional tests to provide a full web browser experience for testing complex JavaScript interactions. This provides a full browser environment using the PhantomJS platform and needs to be installed separately in order to run the tests locally.

PSR-4: Not just a catchy name

Drupal 8 uses the PHP community's excellent coding standard PSR-4. This convention dictates that if a class is named a certain way, is stored in a file with a specific filename, and is within a specific directory structure, the class will be automatically loaded when needed. This saves a little bit of time when writing code for Drupal 8 projects, but most importantly it removes some of the steps necessary to add tests to a project.

Another benefit is that the standard requires using PHP's namespacing system. Instead of having to provide a very verbose class name to avoid possible collision with other classes, using a namespace allows the individual class to have a much shorter name.

While the module's directory structure ends up being a little verbose, it's completely worth the minor hassle to avoid the other problems.

Fewer steps than before

Because of improvements in D8 there are fewer steps to achieve the same results than Drupal 7 required. For example, using PSR-4 means that adding tests to a Drupal 8 module doesn't require any changes to the .info.yml file, skipping step 1 from the D7 tutorial. Also, using the annotation system in Drupal core, step 4 is replaced by a single docblock comment on the class, which was there already.

Step 0: Naming conventions

In this guide the module's name is "mymodule", but it could be anything. The site has a directory named "mymodule" and that directory contains a file named "mymodule.info.yml" and "mymodule.module". Because this is a custom module, as opposed to a contributed module or a Feature, the module is stored in "modules/custom", the .info.yml file is located at "modules/custom/mymodule/mymodule.info.yml" and the .module file is located at "modules/custom/mymodule/mymodule.module".

With those preliminary details out of the way, let us begin.

Step 1: Create the tests file

The next step is to create the actual test file itself. Based upon the information above, of a custom module named "mymodule", the first tests are going to be stored in a file named "BasicTestCase.php" and it will be stored in the "tests" subdirectory. Put together, the codebase might look like the following:

  • core/includes
  • core/misc
  • core/modules
  • modules
    • contrib
      • metatag
      • token
      • ...
    • custom
      • mymodule
        • mymodule.info.yml
        • mymodule.module
        • tests
          • src
            • Functional
              • BasicTestCase.php
  • profiles
  • themes

In this case, the location of the custom module named "mymodule" is "modules/custom/mymodule", thus the test file will be located at "modules/custom/mymodule/tests/src/Functional/BasicTestCase.php".

Step 3: Create the first test class

As mentioned, Drupal 8's testing system uses an object-oriented architecture. Unlike Drupal 7 tests, because of PSR-4 the class doesn't need to be prefixed with the module's name, though it's still useful to add the suffix "TestCase". As a result, all that's left is to use a word or three explaining what the test class is for. For this guide, the test is just going to confirm basic functionality works, so the class can be named "BasicTestCase".

This results in code looking as follows:

<?php

namespace Drupal\Tests\mymodule\Functional;

use Drupal\Tests\BrowserTestBase;

/**
 * Test basic functionality of My Module.
 *
 * @group mymodule
 */
class BasicTestCase extends BrowserTestBase {
}

Once this class is added it will be necessary to rebuild Drupal's caches in order for it to be available to the system.

A few quick things about this test file:

  1. The namespace and directory structure is slightly different to other OOP code in D8. Instead of being under "mymodule/src/Functional" and then having a namespace of "Drupal\mymodule\Functional", the class goes in "mymodule/tests/src" and has the namespace "Drupal\Tests\mymodule\Functional".
  2. The "use" statement loads up the base class the new test extends.
  3. There is no separate "name" value, the class name itself is used in the UI.
  4. The first line of the docblock is used as the class description. Per docblock standards, this comment should be a single sentence and not wrap onto multiple lines. The description is for giving a slightly more verbose explanation of what the tests class is for; usually it should be used to explain what tests the class covers, so that when someone looks at the tests available in the admin interface it's clear what's what.
  5. The '@group' option is used to ... group tests together. This comes into play when running the tests - it's possible to tell Drupal to run all of the tests of a single group at one time, rather than doing them one at a time or rather than running all of the tests at once. It is typical to initially give the group the same name as the module, but in some cases there might be so many tests that they're put into separate groups, e.g. one group for the module's APIs, one for the module's UI functionality, etc. Anyway, for now it's best to just stick with one group with the name of the module.

Step 4: Tell the system about the test

Because Drupal 8 uses PSR-4 there's no need to specifically tell the system that this new test by updating e.g. the module's info.yml, it's all handled automatically. Magic!

Step 5: Set up the tests

A major change in the Drupal 8 test system is that it no longer runs the "standard" installation profile as part of the site setup, instead it runs the "testing" installation profile. This new installation profile doesn't load any of the "normal" modules - node, menu, user, path, taxonomy, views, etc, they're all disabled! All that is actually installed on the "testing" are the two caching modules (page_cache and dynamic_page_cache) and the Classy theme is set as the default theme instead of Bartik. This means that any tests have to take into account the fact that there's basically none of the usual menu structures, links, etc.

For this simple test it'll make life a little easier if core's "node" and "views" modules are enabled. By having these two modules installed the site will show the normal "Welcome to Drupal" front page instead of just a login form.

As with Drupal 7, there's no magic involved in the testing system - if it isn't told specifically what modules to enable then its functionality simply won't be available. This means that yes, when adding a test to a custom "mymodule" module, the tests won't automatically know to enable the "mymodule" module, it has to be told.

  /**
   * {@inheritdoc}
   */
  public static $modules = [
    // Modules for core functionality.
    'node',
    'views',

    // This custom module.
    'mymodule',
  ];

There's one other piece necessary to make the front page work as expected. With the "testing" installation profile there is no front page actually defined - loading the front page just shows a bare login form. In order to fix this the system configuration needs to be updated to tell it what to use as the front page, so an additional step is needed to make it load the expected "/node" page.

  /**
   * {@inheritdoc}
   */
  protected function setUp() {
    // Make sure to complete the normal setup steps first.
    parent::setUp();

    // Set the front page to "/node".
    \Drupal::configFactory()
      ->getEditable('system.site')
      ->set('page.front', '/node')
      ->save(TRUE);
  }

And that's all it takes.

Step 6: Add a test function

There's one missing piece. When the tests are started it checks through all of the classes in the system that extend from the two test classes. However, it won't automatically run them if there isn't this one extra piece.

In order for PHPUnit to run a test class there must be a method that starts with the word "test". This means that there needs to be a method in the new class named "testSomethingThenRunAway", "testLoggingIntoTheSite", "testChangingMyHairColor", "testFizzlepopBerrytwist" - just something that starts with the word "test".

For this guide, the aim is to just make sure the module can be enabled and won't blow up the site. This means that, along with all of the steps above, all that's needed is to add a function with an appropriate name:

  /**
   * Make sure everything works to this point.
   */
  public function testTheSiteStillWorks() {
    // Not doing anything right now, save it for later.
  }

And that's it. Now, starting the new test through either the website or the command line should result in the tests running correctly and not giving any errors. Presuming no errors are found, this means that the module can be enabled successfully!

Progress!

Step 7: Verify the site still loads

One final step to add is a few lines that can confirm that the website still actually works - it's fine that enabling the module doesn't break anything, but it's not quite enough.

For now we'll take an initial step of just loading the site's homepage, and assume that if the homepage is working that nothing went horribly wrong.

  /**
   * Make sure the site still works. For now just check the front page.
   */
  public function testTheSiteStillWorks() {
    // Load the front page.
    $this->drupalGet('<front>');

    // Confirm that the site didn't throw a server error or something else.
    $this->assertSession()->statusCodeEquals(200);

    // Confirm that the front page contains the standard text.
    $this->assertText($this->t('Welcome to Drupal'));
  }

The inline comments explain what each step does, but in short, it just loads the front page and makes sure it isn't completely broken.

This is also why these tests use BrowserTestBase instead of DrupalUnitTestCase. Without having the full Drupal system installed it isn't possible to confirm that enabling the module won't completely break the site.

Putting it all together

Combining all of the changes above will make the module look like this:

modules/custom/mymodule/tests/src/Functional/BasicTestCase.php:

<?php

namespace Drupal\Tests\mymodule\Functional;

use Drupal\Tests\BrowserTestBase;

/**
 * Test basic functionality of My Module.
 *
 * @group mymodule
 */
class BasicTestCase extends BrowserTestBase {

  /**
   * {@inheritdoc}
   */
  public static $modules = [
    // Module(s) for core functionality.
    'node',
    'views',

    // This custom module.
    'mymodule',
  ];

  /**
   * {@inheritdoc}
   */
  protected function setUp() {
    // Make sure to complete the normal setup steps first.
    parent::setUp();

    // Set the front page to "node".
    \Drupal::configFactory()
      ->getEditable('system.site')
      ->set('page.front', '/node')
      ->save(TRUE);
  }

  /**
   * Make sure the site still works. For now just check the front page.
   */
  public function testTheSiteStillWorks() {
    // Load the front page.
    $this->drupalGet('<front>');

    // Confirm that the site didn't throw a server error or something else.
    $this->assertSession()->statusCodeEquals(200);

    // Confirm that the front page contains the standard text.
    $this->assertText($this->t('Welcome to Drupal'));
  }

}

Et voila!

Running tests

Before any tests can be run, a module needs to be enabled. While the machine name of the module is "simpletest", in the modules admin page (admin/modules) it's actually called "Testing" and can be found in the "Core" module group. Enabling this module preps some things in the database and the directory structure,

There are two different ways of running Drupal 8's tests - through the administrative interface when logged in as an administrator, or from the command line using a script that's bundled with Drupal. This article just focuses on using the administrative interface, because it's always there and harder to get lost in than the command line.

To run the tests, go to the configuration dashboard (/admin/config) and click on the "Testing" link listed under the "Development" section. This will present a list of every test available on the site, grouped by the "@group" value from the class docblock, as mentioned above. The first thing to note is that while it is possible to run every single available test in one go, it could take literal hours to do so it's not a good idea. So, instead just focus on the new tests.

Search through the list to find the module the tests are for, i.e. the "@group" value that was set above. All of the tests for this group can now be run in one go just by clicking the checkbox beside it, scrolling down and clicking "Run tests". Alternatively, expand the group by clicking the arrow beside the group name and select the individual tests to run, and again click "Run tests".

When the "Run tests" button is clicked it uses core's Batch API to run all of the processes. The first thing it does is create a barebones installation of Drupal core using the "testing" installation profile and enables the $modules items which are listed. It then runs the setUp() method on the test class to do additional .. setup tasks. Once it's finally all set, it runs all of the test methods in the test class one at a time.

When the tests are all finished it'll show a summary of the results and give a chance to quickly re-run the same tests again, which is simpler than digging through the huge list again. The results section will group the messages by the test class, rather than in one huge list, and it'll hopefully show green lines for each of the steps the testing system made. If there are verbose messages added, which we'll get into next time, a link will be provided to view the HTML file that was created.

And that's all there is to it.

Homework time

A lot can be built from these rudimentary beginnings. Additional lines can be added to confirm that other pages can be loaded, that other text can be confirmed to be shown on the page, etc. Also, additional test methods can be added to group different assertions, e.g. maybe one test method for testing node pages, one for custom pages, etc. It's worth taking some time to adding some additional code to this test class to help feel comfortable with it.

However, for now just start with the above. It is pretty quick to add a test like the above to any and all modules and to submit patches to add such a test to a contrib module, so it's worth taking the time to do this. As mentioned previously having tests available means that the automated tests can be enabled for a module, and it's also a good place to start encouraging others to add more, slowly building the module's test suite.

And that's it for now

The seven (ok, eight) steps have covered quite a lot of important little details. These are the building blocks from which all Drupal 8 tests begin. From these humble beginnings, great things will come, so stay tuned for more articles about testing Drupal!

Thumbnail

Meet team member, Damien McKenna

In his role as Community Lead, Damien directs internal initiatives that strengthen Mediacurrent’s commitment to open-source principles, collaboration, and sharing knowledge to strengthen the Drupal community. Regularly ranked as one...

Learn more about Damien >