PHPUnit Browser test tutorial

Last updated on
4 June 2017

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 example which 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 in your Drupal 8 git checkout. 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 configuration on your current site is 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, 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.

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 is no Reaction Rule yet." 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 next step we need to specify the list of modules that need to be enabled for the test run. In our case this if of course Rules.

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

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}
   */
  public function setUp() {
    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'];

  public 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 in the other hand is executed before each test -function, so you may use it to prepare 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.

And then there are dozens of possible assertions. The easiest of these is $this->assertSession()->pageTextContains($text_to_find_on_page). 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.

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 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 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.

Debugging browser tests

As already mentioned it is very important to enable the browser test debug output so that you can see the pages visited by the browser. If a test fails then PHPUnit stops 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 just use print statements in your test method:

  public function testReactionRulePage() {
    $account = $this->drupalCreateUser(['administer rules']);
    print_r($account->getRoles());
  }

Then the output will looks something like this:

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

EArray
(
    [0] => authenticated
    [1] => bd6p84de
)


Time: 23.35 seconds, Memory: 6.00Mb

There was 1 error:

1) Drupal\Tests\rules\Functional\UiPageTest::testReactionRulePage
This test printed output: Array
(
    [0] => authenticated
    [1] => bd6p84de
)

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

Where to go from here

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