Creating Functional tests (simulated browser)

Last updated on
28 February 2025

This documentation needs work. See "Help improve this page" in the sidebar.

This tutorial will take you through the basics of PHPUnit Browser testing in Drupal 8+. By the end, you should be able to write your first browser test! For this example, we will use the Rules module as an example that contains some functional browser tests. The tutorial will then explain how to test the Rules user interface to ensure it functions properly.

Setup for the tutorial

First, we will need to make sure that the Rules module is installed (download and add it to your modules folder). PHPUnit is part of the Composer dependencies of Drupal core, so make sure you have executed composer install after cloning Drupal using Git. If your site is built upon the drupal/core-recommended project template then you will need to obtain development dependencies (including PHPUnit) by requiring an extra metapackage as described here. Then get yourself familiar with running PHPUnit tests and make sure to enable the BROWSERTEST_OUTPUT_DIRECTORY and printerClass in your phpunit.xml file as explained on that page.

What should you test with BrowserTestBase?

BrowserTestBase gives you a way to test web-based behaviors and interactions. For instance, if you want to verify that a certain permission is required before a user can visit a certain page, you'd create a user without that permission and try to visit the page, and then compare the result to another attempt with that permission.

BrowserTestBase fills a need to perform top-level, request-based tests of integrations between various subsystems in Drupal. Generally, BrowserTestBase tests should not require site-specific themes or configuration, although it's possible to write tests with those requirements. Other tools such as Behat could be used for site-specific behavioral tests.

Finally, BrowserTestBase replaces the legacy Simpletest-based WebTestBase. If you are migrating away from Drupal's Simpletest, you should find it relatively easy to convert WebTestBase tests to BrowserTestBase.

How Drupal's Browser tests work

Most of Drupal is web-oriented functionality, so it's important to have a way to exercise these functions. Browser tests create a complete Drupal installation and a virtual web browser and then use the virtual web browser to walk the Drupal install through a series of tests, just like you would do if you were doing it by hand.

It's terribly important to realize that each test runs in a completely new Drupal instance, which is created from scratch for the test. In other words, none of your configuration and none of your users exists! None of your modules are enabled beyond the default Drupal core modules. If your test sequence requires a privileged user, you'll have to create one (just as you would if you were setting up a manual testing environment from scratch). If modules have to be enabled, you will need to specify them. If something has to be configured, you'll have to write code in the test to do it, because none of the configurations on your current site are in the magically-created Drupal instance that we're testing. None of the files in your files directory are there, none of the optional modules are installed, and none of the users are created.

We have magic commands to do all this within the PHPUnit browser test world, and we'll get to that in a little bit.

Component Diagram

Image source:
Drupadocs

About the Rules module test scenario

The Rules module provides a user interface for administrators where they can create rules. It consists of multiple pages and forms that we want to test, which are protected with user permissions so only admins can access them. The most simple test scenario is in rules/tests/src/Functional/UiPageTest.php.

Figuring out what we need to test

If you install the Rules module, you can manually go through the steps and see what you think needs to be tested.

Visit Configuration > Workflow > Rules where you should see a page saying "There are no enabled reaction rules." in the table of rules. You could add Rules and configure them, but as a first step, we want to test that this admin overview page actually exists and says that there are no rules yet.

Writing a test file

Now it's time to create our tests, which we'll do in the rules/tests/src/Functional folder. PHPUnit will find browser test files automatically in the tests/src/Functional folder of a module.

There are four basic steps involved in building a test:

  • Creating the structure (just creating a class that inherits from \Drupal\Tests\BrowserTestBase or a similar browser test class)
  • Initializing the test case with whatever user creation or configuration needs to be done
  • Creating actual tests within the test case
  • And, of course, trying desperately to figure out why our test doesn't work the way we expect, and debugging the test (and perhaps the module)

To start, we just need a bit of boilerplate extending BrowserTestBase.

	namespace Drupal\Tests\rules\Functional;

use Drupal\Tests\BrowserTestBase;

/**
 * Tests that the Rules UI pages are reachable.
 *
 * @group rules_ui
 */
class UiPageTest extends BrowserTestBase {

As the next step, we need to specify the list of modules that need to be enabled for the test run. In our case, this is of course Rules.

	  /**
   * Modules to enable.
   *
   * @var array
   */
  protected static $modules = ['node', 'rules'];

We also need to specify the default theme which should be enabled. It is mandatory to specify a default theme since release 8.8. Read more about it here. Since in our case we aren't relying on any core markup we will use stark as the default theme.

	  /**
   * Theme to enable.
   *
   * @var string
   */
  protected $defaultTheme = 'stark';

Next comes the optional setUp(). Here is where we must do anything that needs to be done to make this Drupal instance work the way we want to. We have to think: "What did I have to do to get from a stock Drupal install to where I can run this test?". Not every test case will need this, but here is an example where we prepare a content type (taken from Rules' ConfigureAndExecuteTest):

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

    // Create an article content type that we will use for testing.
    $type = $this->container->get('entity_type.manager')->getStorage('node_type')
      ->create([
        'type' => 'article',
        'name' => 'Article',
      ]);
    $type->save();
    $this->container->get('router.builder')->rebuild();
  }

Note, that if you implement setUp()-method, start with executing the parent::setUp()-method like in the example.

Create specific test: the reaction rule page

Now we need to create specific tests to exercise the module. We just create methods of our test class, each of which exercises a particular test. All methods should start with 'test' in lower-case. Any method with public visibility that starts this way will automatically be recognized by PHPUnit and run when requested.

Our first test will check the page at admin/config/workflow/rules:

	  /**
   * Tests that the reaction rule listing page works.
   */
  public function testReactionRulePage() {
    $account = $this->drupalCreateUser(['administer rules']);
    $this->drupalLogin($account);

    $this->drupalGet('admin/config/workflow/rules');
    $this->assertSession()->statusCodeEquals(200);

    // Test that there is an empty reaction rule listing.
    $this->assertSession()->pageTextContains('There is no Reaction Rule yet.');
  }

setUp() and individual tests

Each test function will have a completely new Drupal instance to execute tests. This means that whatever you have created in a previous test function will not be available anymore in the next.

Consider this example, taken from Rules UIPageTest.php -extraction:

	namespace Drupal\Tests\rules\Functional;

class UiPageTest extends RulesBrowserTestBase {

 /**
   * Modules to enable.
   *
   * @var array
   */
  protected static $modules = ['rules'];

  /**
   * {@inheritdoc}
   */
  protected function setUp() {
    parent::setUp();
    // ....
  }
  public function testReactionRulePage() {
    $account = $this->drupalCreateUser(['administer rules']);
    $this->drupalLogin($account);
    // .....
  }

  public function testCreateReactionRule() {
    $account = $this->drupalCreateUser(['administer rules']);
    $this->drupalLogin($account);
    // .....
  }

  public function testCancelExpressionInRule() {
    // Setup a rule with one condition.
    $this->testCreateReactionRule();
    // .....
  }

}

Here both testReactionRulePage() and testCreateReactionRule must create their own $account, because both tests are run against their own Drupal instances and the latter function's Drupal has no accounts unless created.

However, other (later) tests can rely on content created in other test functions if they execute those functions separately like testCancelExpressionInRule() does.

setUp()-function on the other hand is executed before each test -function, so you may use it to prepare the test environment as well.

drupalGet() and Assertions

The code above did a very simple GET request on the admin/config/workflow/rules page. It loads the page, then checks the response code and asserts that we find appropriate text on the page.

Most tests will follow this pattern:

  1. Do a $this->drupalGet('some/path') to go to a page
  2. Use $this->clickLink(..) to navigate by links on the page
  3. Use $this->getSession()->getPage()->fillField(...); to fill out form fields
  4. Submit forms with $this->getSession()->getPage()->pressButton(...); or use $this->submitForm(...); (or use the deprecated drupalPostForm() method)
  5. Do one or more assertions to check that what we see on the page is what we should see.

Filling in forms

	    $page = $this->getSession()->getPage();

    $page->fillField('form_element', 'value');
    $page->selectFieldOption('form_element', 'value');

Assertions

There are dozens of possible assertions. Some examples are below. When you get beyond this tutorial, you'll want to look at the \Behat\Mink\WebAssert class and its Drupal child \Drupal\Tests\WebAssert to read about more of them.

	    $this->assertOptionSelected('form_element', 'value');
    $this->assertFieldByName('form_element');

    $web_assert = $this->assertSession();

    $web_assert->addressEquals('node/1');
    $web_assert->pageTextContains("Expected text");
    $web_assert->pageTextNotContains("Unexpected text");

Running the test

We will use the command line to execute the test. This is documented in more detail on the running PHPUnit tests page.

Let's execute just the testReactionRulePage() method of UiPageTest:

	cd core
../vendor/bin/phpunit --filter testReactionRulePage ../modules/rules/tests/src/Functional/UiPageTest.php
PHPUnit 4.8.11 by Sebastian Bergmann and contributors.

.

Time: 25.14 seconds, Memory: 6.00Mb

OK (1 test, 5 assertions)

HTML output was generated
http://drupal-8.localhost/sites/simpletest/browser_output/Drupal_Tests_rules_Functional_UiPageTest-13-411368.html
http://drupal-8.localhost/sites/simpletest/browser_output/Drupal_Tests_rules_Functional_UiPageTest-14-411368.html
http://drupal-8.localhost/sites/simpletest/browser_output/Drupal_Tests_rules_Functional_UiPageTest-15-411368.html

Yay, our test has passed! It also generated a couple of HTML output files, where you can see the pages the browser visited during the test. The pages are written to files linked as for example above, so you can inspect them after the test has run.

A demonstration failing test

It really doesn't teach us much to just have a test that succeeds. Let's look at one that fails.

We'll modify the test to provoke a test fail - we will assert that the page contains some different text, which is of course not there.

	  /**
   * Tests that the reaction rule listing page works.
   */
  public function testReactionRulePage() {
    $account = $this->drupalCreateUser(['administer rules']);
    $this->drupalLogin($account);

    $this->drupalGet('admin/config/workflow/rules');
    $this->assertSession()->statusCodeEquals(200);

    $this->assertSession()->pageTextContains('some text not actually on the page');
  }

Run the test and see the result:

	../vendor/bin/phpunit --filter testReactionRulePage ../modules/rules/tests/src/Functional/UiPageTest.php 
PHPUnit 4.8.11 by Sebastian Bergmann and contributors.

E

Time: 24.38 seconds, Memory: 6.00Mb

There was 1 error:

1) Drupal\Tests\rules\Functional\UiPageTest::testReactionRulePage
Behat\Mink\Exception\ResponseTextException: The text "some text not actually on the page" was not found anywhere in the text of the current page.

/home/klausi/workspace/drupal-8/vendor/behat/mink/src/WebAssert.php:787
/home/klausi/workspace/drupal-8/vendor/behat/mink/src/WebAssert.php:262
/home/klausi/workspace/drupal-8/modules/rules/tests/src/Functional/UiPageTest.php:37

FAILURES!
Tests: 1, Assertions: 5, Errors: 1.

HTML output was generated
http://drupal-8.localhost/sites/simpletest/browser_output/Drupal_Tests_rules_Functional_UiPageTest-16-425496.html
http://drupal-8.localhost/sites/simpletest/browser_output/Drupal_Tests_rules_Functional_UiPageTest-17-425496.html
http://drupal-8.localhost/sites/simpletest/browser_output/Drupal_Tests_rules_Functional_UiPageTest-18-425496.html

Oops, something went wrong and our test has caught that. Yay!

When to use t() in browser tests

Never! Nope, not in assertion messages, not for button labels, not for the text you assert on the page. You always want to test the literal string on the page, you don't want to test the Drupal translation system.

Browser tests in Drupal core do use t() but only when specifically testing translations.

Debugging browser tests

As already mentioned it is very important to enable debug output in the browser test so that you can see the pages visited by the browser. If a test fails then PHPUnit stops the execution of the test where it fails, which means the last of the HTML output links is the page where the error occurred.

You can also use the dump() function (provided by the Symfony VarDumper component), in either your test method, or the site code that is being run. This has the advantage over using print or print_r() that its output is not detected as a test error.

You can also use Devel module's ddm() method to output to a logfile. Configure the location of the logfile in your test code like this:

	    $config = $this->config('devel.settings');
    $config->set('debug_logfile', '/path/to/file/drupal_debug.txt')->save();
    $config->set('debug_pre', FALSE)->save();

Writing a base class to be extended by other test classes

You may find that some of your test duplicate code for shared functionality. In this case, you may want to create a base class that is then extended by your various test classes. When doing this there are 2 important things to keep in mind.

  1. The base test class should be abstract
  2. The base test class should end with the word Base

Example base test class: my_module/Tests/src/Functional/myModuleTestBase.php

	<?php

namespace Drupal\Tests\my_module\Functional;

/**
 * To be extended by other test classes.
 */
abstract class myModuleTestBase extends BrowserTestBase {
  protected function setUp() {
    parent::setUp();
    // etc
  }
}

Extending the base class: my_module/Tests/src/Functional/myModuleSomethingTest.php

	<?php

namespace Drupal\Tests\my_module\Functional;

/**
 * Test something about my module.
 *
 * @group my_module
 */
class myModuleSomethingTest extends myModuleTestBase {

  protected function setUp() {
    parent::setUp();
  }

  public function testSomething() {
    // do the test.
  }
}

SQLite

Using an SQLite database makes testing faster than with a DBMS like MySQL. Because the database is contained in just 1 file. Make sure that Drupal is not using your default database connection from the settings.php file.

A simple solution is to check the HTTP_USER_AGENT. E.g.:

	if ($_SERVER['HTTP_USER_AGENT'] !== 'Drupal command line') {

  $databases['default']['default'] = [
    'database' => 'MY-DATABASE',
    'username' => 'root',
    'password' => 'root',
    'prefix' => '',
    'host' => '127.0.0.1',
    'port' => '',
    'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
    'driver' => 'mysql',
    'unix_socket' => '/Applications/MAMP/tmp/mysql/mysql.sock',
  ];

}

In your phpunit.xml file specify your SQLite database connection (the file must already exist):

	<env name="SIMPLETEST_DB" value="sqlite://localhost/sites/default/files/db.sqlite"/>

Otherwise, you get errors like this:

	Exception: PDOException: SQLSTATE[HY000] [2002] Connection refused

Using session data

You can inspect session data for the most recent request as follows:

	    $session_data = $this->container->get('session_handler.write_safe')->read($this->getSession()->getCookie($this->getSessionName()));
    $session_data = unserialize(explode('_sf2_meta|', $session_data)[1]);

Where to go from here

Link further information about browser tests here, as you find them!

Help improve this page

Page status: Needs work

You can: