Writing and running software tests has become the "norm" in the current software development lifecycle: It helps you to prove to the outside world that the software you wrote works the way it should be, and helps you to test if a certain functionality works every time you do changes to your code.

It may sound trivial to write blocks of extra code to prove that your software works, even though you did manual testing on your software and you made sure that It worked as expected, but writing tests will save you lots of time when doing changes in your software because you can run the tests to make sure that the software actually works and thus, avoid some of the manual testing that is covered by the automated tests. 

Drupal has adopted PHPUnit, a leading testing framework, replacing the older Simpletest framework. This shift to PHPUnit with Drupal versions 8, 9, and 10 ensures that developers have a robust, modern tool at their disposal for writing various types of tests.

In this article, you will learn how to write simple tests for your custom modules, and it is a starting point for writing more complex tests for your Drupal software needs.

Understanding PHPUnit in Drupal

What is PHPUnit?

PHPUnit is a programmer-oriented testing framework for PHP, designed to write and run tests in a structured way. Drupal integrates PHPUnit to support different types of tests:

  • Unit tests: Allows you to test specific snippets of code that don't require database connection to the Drupal site.
  • Kernel tests: Useful for testing module/core APIs and integrations with a bootstrapped kernel and a minimal number of modules enabled.
  • Functional tests: Unit tests with a full bootstrapped Drupal instance, you'll need access to the DB in order to run these tests.
  • Javascript tests: Unit tests to perform Javascript and AJAX functionality in the browser.

Each test type is suited to different testing needs, providing comprehensive coverage across all aspects of application development. You can learn more about the types of tests that are available in the Drupal context.

Setting Up PHPUnit in Your Drupal Project

Installation

PHPUnit is not included out-of-the-box when you create a new Drupal project with composer, you need to install it in your project, luckily, the drupal/core-dev package comes with this tool, and you can install it with composer:

 

$ composer require drupal/core-dev --dev --update-with-all-dependencies

Configuration

After installation, you still need to do some extra configuration to use PHPUnit for your Drupal project by setting several local variables:

  • SIMPLETEST_BASE_URL: URL of the site you want to test, for example "https://localhost/"
  • SIMPLETEST_DB: Database connection string for your site, for example, "mysql://db:db@db/db"
  • BROWSER_OUTPUT_DIRECTORY: Directory used by PHPUnit to store the output of the unit tests, for example, "/var/www/html/phpunit-output"

 

You can set these variables in the file web/core/phpunit.xml.dist or set these environment variables in your shell of the hosting environment:

 

$  export SIMPLETEST_BASE_URL="https://localhost/"
$  export SIMPLETEST_DB="mysql://db:db@db/db"
$  export BROWSER_OUTPUT_DIRECTORY="/var/www/html/phpunit-output"				

 

Verify Installation 

Run PHPUnit to ensure it's correctly set up:

 

$ ./vendor/bin/phpunit

Running phpunit

 

This output doesn't say a lot (for now), I'll show how to create a very basic Unit test and how to run it.

Creating and Running Basic PHPUnit Tests

The snippet below is a very basic PHPUnit Functional test for the Drupal Contrib module Google Calendar Service, this module provides the functionality to create the calendar and sync google calendar events in the Drupal system.:

 

<?php
namespace Drupal\Tests\google_calendar_service\Functional;
use Drupal\Tests\BrowserTestBase;
class GoogleCalendarServiceTest extends BrowserTestBase {
  /**
   * Modules to enable.
   *
   * @var array<string>
   */
  protected static $modules = ['google_calendar_service'];
  /**
   * Theme to enable. This field is mandatory.
   *
   * @var string
   */
  protected $defaultTheme = 'stark';
  /**
   * The simplest assert possible.
   */
  public function testOne() {
    $this->assertTrue(TRUE);
  }
}

 

The snippet above creates a new class, GoogleCalendarServiceTest, which extends the base class BrowserTestBase (we are creating a Functional Browser test), with two protected properties that need to be set: $modules and $defaultTheme:

 

  • $modules:  Modules to use during the testing process
  • $defaultTheme: Theme that will be enabled during the testing process, this variable is mandatory, let's use the 'stark' theme for this.

 

The most important piece of the class is the public method testOne(), which is the actual test to run:

 

  public function testOne() {
    $this->assertTrue(TRUE);
  }

 

We can see that there is a call to the base method assertTrue(), this method is a builtin function in PHPUnit and is used to assert whether the assert value is TRUE or not. This assertion will return TRUE in the case if the assert value is TRUE else returns FALSE. In this simple test case, for illustrative purposes, the assertTrue() function will always return TRUE since we are passing the TRUE value to the method.

An assertion, in the PHPUnit context, is a builtin method to check if the value generated during the test is what we are expecting during the test execution, for example, we can run an assertTrue() function to check if the config form of the module has the fields that we need or not.

There is a good chunk of information about what an assertion is and the list of assertion functions in the official PHPUnit documentation.

 

Going back to our sample test, we will store the Unit test class in the file google_calendar_service/tests/src/Functional/GoogleCalendarServiceTest.php.

After that, we'll use this command line to execute the test (from the root of your project's directory):

 

$ ./vendor/bin/phpunit web/modules/contrib/google_calendar_service/tests/src/Functional/GoogleCalendarServiceTest.php --verbose -c /var/www/html/web/core/phpunit.xml.dist

 

Output of the execution of the test:


Execution of the Unit Test

We can see that the test was executed with no issues, the simple test was passed, it took about 3 seconds to execute, and we didn't get any warnings.

 

You can create as many Unit tests (Classes) as you want and test them.

Creating and Running Advanced PHPUnit Tests

As developers build more complex features, tests need to reflect these complexities. For instance, testing a configuration form within the Google Calendar Service module could involve verifying form submissions and user interactions. 

Our Contrib Module, Google Calendar Service, has a config form to store the module settings, which can be reached by going to /admin/config/google-calendar-service/settings :

 

Google Calendar Service config page

 

This form has some fields to save the data needed for the Google Calendar import (Google Secret File and Google User Email), now, the question is, how can we test this form?  how can we make sure that the form will save the field data if we click on "Save configuration"?

Let's rewrite the class below to include a few methods to test the form:

 

<?php
namespace Drupal\Tests\google_calendar_service\Functional;
use Drupal\Tests\BrowserTestBase;
class GoogleCalendarServiceTest extends BrowserTestBase {
  /**
   * Modules to enable.
   *
   * @var array<string>
   */
  protected static $modules = ['google_calendar_service'];
  /**
   * Theme to enable. This field is mandatory.
   *
   * @var string
   */
  protected $defaultTheme = 'stark';
  /**
   * A test user with permission to access the google calendar service config page.
   *
   * @var \Drupal\user\UserInterface
   */
  protected $adminUser;
  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    // Make sure to complete the normal setup steps first.
    parent::setUp();
    // Create and log in an administrative user.
    $this->adminUser = $this->drupalCreateUser([
      'administer calendars and events',
    ]);
    $this->drupalLogin($this->adminUser);
  }
  /**
   * The simplest assert possible.
   */
  public function testOne() {
    // Load the front page.
    $this->drupalGet('<front>');
    // Confirm that the site didn't throw a server error or something else.
    $this->assertSession()->statusCodeEquals(200);
  }
  /**
   * Test the admin page.
   */
  public function testAdminPage() {
   // Load the destination page.
   $this->drupalGet('admin/config/google-calendar-service/settings');
   $session = $this->assertSession();
   $session->statusCodeEquals(200);
   $session->pageTextContains('Secret Client File Step');
   $session->pageTextContains('Google User Email');
   $session->pageTextContains('Secret Client File');
   $session->fieldExists('google_user_email');
   $session->buttonExists('Save configuration');
    // Create a new source of type File Directory.
    $edit = [
      'google_user_email' => 'googletest@gmail.com',
    ];
    $this->submitForm($edit, 'Save configuration');
  }
}

 

We see two additional methods:

 

  • setUp(): This function has the task of creating a test admin user (with some permissions) to check access to config form, and logs to the site
  • testAdminPage(): This function tries to load the config form page and checks if it loaded correctly (returns status code 200), then checks if the two fields were rendered by searching for some strings that should've been displayed in the form, and finally it tries to emulate the form submit process.

 

In addition to that, the method testOne() was rewritten to load the front page and confirms if the site didn't throw an error or something.

 

Let's execute the test again:

 

Execution of the test

 

We can see that the two tests (testAdminPage and testOne) were executed with no warnings/errors, and It took about 11 seconds to execute the two tests.

Now, what if we want to test if the form has a string that doesn't exist there (RANDOM RANDOM), what would I get if I run the test:

 

   public function testAdminPage() {
   // Load the destination page.
   $this->drupalGet('admin/config/google-calendar-service/settings');
   $session = $this->assertSession();
   $session->statusCodeEquals(200);
   $session->pageTextContains('Secret Client File Step');
   $session->pageTextContains('Google User Email');
   $session->pageTextContains('Secret Client File RANDOM RANDOM');
   $session->fieldExists('google_user_email');
   $session->buttonExists('Save configuration');
    // Create a new source of type File Directory.
    $edit = [
      'google_user_email' => 'googletest@gmail.com',
    ];
    $this->submitForm($edit, 'Save configuration');
  }
}

 

We run the test again:

 

Execution of the test with an error

 

We can see that we got an error, which was expected because the text doesn't exist anywhere in the form.

Summary

This article is a glimpse of that PHPUnit is and why you should use it during the Drupal Development cycle, in addition to that we have showcased two testing scenarios for a Drupal Contrib module, and how to implement and run the test cases. We encourage you to read the official PHPUnit documentation to get familiarized with the PHPUnit jargon and get to know the list of the available assertions for your testing needs.

References

Development

We do Drupal development

Go to our Drupal page!

Visit page!

Browse cities

Recommended Stories

Drupal: Age 1-11 in a Nutshell
For over two decades, Drupal has evolved from a simple message board to one of the most powerful and versatile… (Read more)
10 minutes /
Drupal Security Best Practices: Protecting Your Website from Common Threats
As Drupal and other technologies have grown, so have the stakes for keeping websites secure. Security isn’t a… (Read more)
10 minutes /
The Future of Drupal: What’s Next in Web Technologies
The digital world never sits still, and neither should your website. As users demand faster, smarter, and more… (Read more)
10 mins /