A sneak peek of my Drupal automated testing course

Happy Christmas!

Here's my present - a sneak peek at the first lesson in my free upcoming Automated Testing for Drupal email course.

In this lesson, we start from scratch and end with a working test suite.

If you like it, register for free and get the full course when it launches.

Creating a Drupal project

If you don't have one, you'll need to create a new Drupal project. I'd suggest using Drupal 10.2 and the instructions at https://www.drupal.org/download.

You'll need PHP and Composer.

First, run composer create-project drupal/recommended-project drupal followed by cd drupal && composer require --dev drupal/core-dev to add the development dependencies, including PHPUnit.

At this point, you should have a web directory and a phpunit file within vendor/bin.

Finally, run php -S 0.0.0.0:8000 -t web to start a local web server.

You don't need to install Drupal - as long as you see the installation page, that's fine.

Creating a custom module

Before adding tests, you must create a module to place them in.

Run mkdir -p web/modules/custom/atdc to create an empty module directory, and create an atdc.info.yml file within it with this content:

name: Example
type: module
core_version_requirement: ^10
package: Custom

This is the minimum content needed for a module to be installable.

Writing your first test class

Test classes are placed within each module's tests/src directory.

Run mkdir -p web/modules/custom/atdc/tests/src/Functional && touch web/modules/custom/atdc/tests/src/Functional/ExampleTest.php to create the directory structure and a blank test class.

Then, add this content.

<?php

namespace Drupal\Tests\atdc\Functional;

use Drupal\Tests\BrowserTestBase;
use Symfony\Component\HttpFoundation\Response;

class ExampleTest extends BrowserTestBase {

  public $defaultTheme = 'stark';

}

Note: within a test class, the namespace is Drupal\Tests\{module_name} instead of Drupal\{module_name}.

With the boilerplate class added, create a test method within it:

public function testBasic(): void {
  self::assertTrue(FALSE);
}

Note: the class name must be suffixed with Test, and the test method must be prefixed with test for them to be run.

Now we have a test with an assertion, we need to run it and see if it passes.

Running the test

On the command line, run vendor/bin/phpunit web/modules/custom, and you'll get an error like:

PHPUnit\TextUI\RuntimeException: Class "Drupal\Tests\BrowserTestBase" not found.

This isn't an assertion failure, but that PHPUnit can't find the files it needs to run.

To fix this, let's configure PHPUnit.

Configuring PHPUnit

Create a new phpunit.xml.dist file at the root of your project, with this content:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="web/core/tests/bootstrap.php" colors="true">
  <php>
    <env name="SIMPLETEST_BASE_URL" value="http://localhost:8000"/>
    <env name="SIMPLETEST_DB" value="sqlite://localhost//dev/shm/test.sqlite"/>
    <ini name="error_reporting" value="32767"/>
    <ini name="memory_limit" value="-1"/>
  </php>
  <testsuites>
    <testsuite name="Example tests">
      <directory suffix="Test.php">./web/modules/**</directory>
    </testsuite>
  </testsuites>
</phpunit>

This is based on web/core/phpunit.xml.dist with some project-specific changes.

Namely, setting the bootstrap value to include the web/core path and fix the error, and populating the SIMPLETEST_BASE_URL and SIMPLETEST_DB environment variables.

PHPUnit now knows where the files are, to connect to Drupal at http://localhost:8000 (matching the PHP web server address) and an SQLite database.

I've also added a testsuite that declares where any test classes will be located so the path doesn't need to be specified on the command line.

Re-running the tests

Running the tests again will give the expected error about a failing assertion:

Failed asserting that false is true.

Fix the assertion in the test by changing FALSE to TRUE, run vendor/bin/phpunit again, and you should see a passing test.

OK (1 test, 2 assertions)

Improving the tests

Now you have as passing test and know PHPUnit is working, let's improve it.

Instead of the basic check, let's check whether certain pages exist and are accessible.

To keep things simple and focused on writing and running tests, let's use some standard Drupal pages - the front and administration pages instead of writing your own.

As you're writing functional tests by extending BrowserTestBase, you can make HTTP requests to the web server, and make assertions on the responses.

Replace the testBasic test method with the following:

public function testFrontPage(): void {
  $this->drupalGet('/');

  $assert = $this->assertSession();
  $assert->statusCodeEquals(Response::HTTP_FORBIDDEN);
}

public function testAdminPage(): void {
  $this->drupalGet('/admin');

  $assert = $this->assertSession();
  $assert->statusCodeEquals(Response::HTTP_OK);
}

These tests will make HTTP requests to the specified paths and assert the status code on the response matches the expected values.

I'm using the constants on the Response class, but you can also use the status code numbers - e.g. 200 and 403.

Running the updated tests

Running vendor/bin/phpunit, you'll get two errors:

1) Drupal\Tests\atdc\Functional\ExampleTest::testFrontPage Behat\Mink\Exception\ExpectationException: Current response status code is 200, but 403 expected.

2) Drupal\Tests\atdc\Functional\ExampleTest::testAdminPage Behat\Mink\Exception\ExpectationException: Current response status code is 403, but 200 expected.

ERRORS!
Tests: 2, Assertions: 4, Errors: 2.

The responses are not returning the expected status codes, so the tests are failing.

Reviewing them, the front page should return a 200 response code (HTTP_OK) as it's accessible to all users, including anonymous users.

As we're logged out, the administration page should return a 403 (HTTP_FORBIDDEN).

Swapping the assertions should get the tests to pass.

Now, running vendor/bin/phpunit returns no errors or failures.

OK (2 tests, 4 assertions)

Congratulations!

Conclusion

In this lesson, you've created a new Drupal 10 project, configured PHPUnit and created a custom module with your first passing browser tests.

From this, you can hopefully see that automated testing doesn't need to be difficult, and the configuration you've done here will work for the upcoming lessons, where you'll expand on what you've done and explore more that Drupal and PHPUnit have to offer.

I hope you enjoyed this sneak peek, and if you'd like to receive the course once it's complete, register here for free.

- Oliver

Was this interesting?

Sign up here and get more like this delivered straight to your inbox every day.

About me

Picture of Oliver

I'm an Acquia-certified Drupal Triple Expert with 17 years of experience, an open-source software maintainer and Drupal core contributor, public speaker, live streamer, and host of the Beyond Blocks podcast.