Author image
Developer

How to write a PHPUnit functional test for Drupal 8

This article will talk you through the steps to follow to write a simple PHPUnit functional (Kernel) test for Drupal 8.

I have been doing a lot of work on Drupal 8 migrations for the past few months so that will be the focus of the test.

Step 1: Create a Fixture

To quote the PHPUnit manual:

One of the most time-consuming parts of writing tests is writing the code to set the world up in a known state and then return it to its original state when the test is complete. This known state is called the fixture of the test.

In the case of testing a Drupal migration, the fixture is a database agnostic[1] representation of the data source to be migrated. According to the convention set out in core, the file should be created in the following location: modules/[module_name]/tests/fixtures and the file name should reference the version of Drupal core to which it applies, e.g. drupal7.php. It mainly consists of createTable() and insert() commands that form the dataset.

The db-tools.php command line application can be used to automate the creation of a fixture file. This was used in core to create the fixture files for the migrate_drupal module. To find out more, I recommend reading the drupal.org article entitled Generating database fixtures for D8 Migrate tests.

Alternatively you can write the fixture file manually - if doing so, core/modules/migrate_drupal/tests/fixtures/drupal7.php is a good point of reference.

Here is an (admittedly convoluted) example of a fixture file:

<?php
/**
 * @file
 * A database agnostic dump for testing purposes.
 */

use Drupal\Core\Database\Database;

$connection = Database::getConnection();

$connection->schema()->createTable('example', array(
  'fields' => array(
    'id' => array(
      'type' => 'serial',
      'not null' => TRUE,
    ),
    'name' => array(
      'type' => 'varchar',
      'not null' => TRUE,
      'length' => '255',
      'default' => '',
    ),
    'weight' => array(
      'type' => 'int',
      'not null' => TRUE,
      'default' => '0',
    ),
  ),
  'primary key' => array(
    'id',
  ),
  'mysql_character_set' => 'utf8',
));

$connection->insert('example')
->fields(array(
  'id',
  'name',
  'weight',
))
->values(array(
  'id' => '1',
  'name' => 'General discussion',
  'weight' => '2',
))
->values(array(
  'id' => '2',
  'name' => 'Term1',
  'weight' => '0',
))
->execute();

The snippet above creates one table (example) containing 3 fields (id, name, weight) and then inserts two records into it. Depending on the complexity of the test, a fixture file could include any number of tables each containing enough rows to cater for each facet that needs to be tested.

Step 2: Create the test file

This is the file that will contain the method to set up and run the test(s).

The file structure of test files is important, our example would be created in modules/[module_name]/tests/src/Kernel/Migrate/d7. It is located in the Kernel directory because it is a functional test - this is explained in more details in the drupal.org Automated tests article. It is then organised into the Migrate directory (obviously because it is a migration test). For more details on the location of test files read the drupal.org article on PHPUnit file structure, namespace, and required metadata.

The file name (and contained class) should end in "Test" and for migration tests should begin with "Migrate" (according to Drupal core convention). In our example the file will be called MigrateExampleTest.php and will initially contain the following:

<?php

namespace Drupal\Tests\example\Kernel\Migrate\d7;

use Drupal\Tests\migrate_drupal\Kernel\MigrateDrupalTestBase;

/**
 * Tests 'example' migration.
 *
 * @group example
 */
class MigrateExampleTest extends MigrateDrupalTestBase {

  static $modules = ['example'];

  /**
   * [email protected]}
   */
  protected function setUp() {
    parent::setUp();
  }

  /**
   * Test 'example' migration from Drupal 7 to 8.
   */
  public function testExample() {

  }
}

The class name should match the file name, in this case MigrateExampleTest. A straightforward test class should extend MigrateDrupalTestBase, but a more complicated test class could extend MigrateDrupal7TestBase which provides access to the migrate_drupal fixture including a well-populated database.

The test class must specify a @group that matches the originating module short name. $modules is an array of modules that need to be enabled for this test (including this migration module). The setUp() and testExample() methods will be covered in detail in the following sections.

Step 3: Write a setUp() method

The setUp() method contains the code required to prepare and run the test(s). Here is the straightforward class for our example migration:

protected function setUp() {
  parent::setUp();
  $this->loadFixture( __DIR__ . '/../../../../tests/fixtures/drupal7.php');

  $this->executeMigrations(['example']);
}

You are likely to want to call the setUp() method of the parent class very early, if not the first line of the method. This will run various vital bootstrapping commands, such as setting up the database connection. You should then load the fixture (as created in Step 1).

You may also need to call any/all of the following methods, but they will not be covered within this basic tutorial:

  • $this->installEntitySchema()
  • $this->installSchema()
  • $this->installConfig()

Finally call $this->executeMigrations() with an array of migration IDs. Our version only executes the migration with ID example, but this array can contain any number of migration IDs.

Step 4: Write the test method(s)

Our simple class only contains a single test method, testExample(), but there is no limit on the number of test methods. Test methods must be public and named test*. They contain a number of assertion methods, e.g. $this->assertSame(), to assert that an actual value matches an expected value, see Appendix A of the PHPUnit manual for more details. Here is our test method:

/**
 * Test 'example' migration from Drupal 7 to 8.
 */
public function testExample() {
  $example = Example::load(1);
  $this->assertTrue($example instanceof Example);
  $this->assertEquals('General discussion', $example->name);

  $example2 = Example::load(2);
  $this->assertTrue($example2 instanceof Example);
  $this->assertEquals(0, $example->weight);
}

Step 5: Test your tests

And with that you have written a PHPUnit test for Drupal! But that is not all there is to it. Don't just leave it there, don't just blindly upload this to drupal.org as part of a module, run the tests locally first. I found the articles on drupal.org useful, if you are new to running PHPUnit tests then I highly recommend you start there.

Glossary

[1] Database agnostic - able to work with various systems, rather than being customised for a single system.