php[architect] logo

Want to check out an issue? Sign up to receive a special offer.

Testing Your Drupal Site with Behat

Posted by on August 9, 2016
Screenshot of Behat tests results

If automated testing is not already part of your development workflow, then it’s time to get started. Testing helps reduce uncertainty by ensuring that new features you add to your application do not break older features. Having confidence that your not breaking existing functionality reduces time spent hunting bugs or getting reports from clients by catching them earlier.

Unfortunately, testing still does not get the time and attention it needs when you’re under pressure to make a deadline or release a feature your clients have been asking for. But—like using a version control system and having proper development, staging, and production environments—it should be a routine part of how you do your work. We are professionals, after all. After reading all the theory, I only recently took the plunge myself. In this post, I’ll show you how to use Behat to test that your Drupal site is working properly.

Before we dive in, the Behat documentation describes the project as:

[…] an open source Behavior Driven Development framework for PHP 5.3+. What’s behavior driven development, you ask? It’s a way to develop software through a constant communication with stakeholders in form of examples; examples of how this software should help them, and you, to achieve your goals.

Basically, it helps developers, clients, and others communicate and document how an application should behave. We’ll see shortly how Behat tests are very easy to read and how you can extend them for your own needs.

Mink is an extension that allows testing a web site by simulating interacting with it through a browser to fill out form fields, click on links, and so forth. Mink lets you test via Goutte, which makes requests and parses the contents but can’t execute JavaScript. It can also use Selenium, which controls a real browser and can thus test JS and Ajax interactions, but Selenium requires more configuration.

Requirements

To get started, you’ll need to have Composer on your machine. If you don’t already, head over to the Composer Website. Once installed, you can add Behat, Mink, and Mink drivers to your project by running the following in your project root:

composer require behat/behat
composer require behat/mink
composer require behat/mink-selenium2-driver
composer require behat/mink-extension

Once eveything runs, you’ll have a composer.json file with:

    "require": {
        "behat/behat": "^3.1",
        "behat/mink": "^1.7",
        "behat/mink-selenium2-driver": "^1.3",
        "behat/mink-extension": "^2.2"
    },

This will download Behat and it’s dependencies into your vendor/ folder. To check that it works do:

vendor/bin/behat -V

There are other ways to install Behat, outlined in the quick introduction.

The Drupal community has a contrib project, Behat Drupal Extension, that is an integration for Behat, Mink, and Drupal. You can install it with the requre command below. I had to specify the ~3.0 version, otherwise composer couldn’t satisfy dependencies.

composer require drupal/drupal-extension:~3.0

And you’ll have the following in your composer.json:

       "drupal/drupal-extension": "~3.0",

Configuring Behat

When you run Behat, it’ll look for a file named behat.yml. Like Drupal 8, Behat uses YAML for configuration. The file tells Behat what contexts to use. Contexts provide the tests that you can run to validate behavior. The file configures the web drivers for Mink. You can also configure a region_map which the Drupal extension uses to map identifiers (left of the :) to CSS selectors to identify theme regions. These come in very handy when testing Drupal theme output.

The one I use looks like:

default:
  suites:
    default:
      contexts:
        - Drupal\DrupalExtension\Context\DrupalContext
        - Drupal\DrupalExtension\Context\MarkupContext
        - Drupal\DrupalExtension\Context\MessageContext
        - FeatureContext
  extensions:
    Behat\MinkExtension:
      goutte: ~
      javascript_session: selenium2
      selenium2:
        wd_host: http://local.dev:4444/wd/hub
        capabilities: {"browser": "firefox", "version": "44"}
      base_url: http://local.dev
    Drupal\DrupalExtension:
      blackbox: ~
      region_map:
        breadcrumb: '#breadcrumb'
        branding: '#region-branding'
        branding_second: '#region-branding-second'
        content: '#region-content'
        content_zone: '#zone-content'
        footer_first: '#region-footer-first'
        footer_second: '#region-footer-second'
        footer_fourth: '#region-footer-fourth'
        menu: '#region-menu'
        page_bottom: '#region-page-bottom'
        page_top: '#region-page-top'
        sidebar_first: '#region-sidebar-first'
        sidebar_second: '#region-sidebar-second'

Writing a Simple Feature

Now comes the fun part. Let’s look at writing a feature and how to test that what we expect is on the page. The first time we run it, we need to initialize Behat to generate a FeatureContext class. Do so with:

vendor/bin/behat --init

That should also create a features/ directory, where we will save the features that we write. To behat, a feature is test suite. Each test in a feature evaluates specific functionality on your site. A feature is a text file that ends in .feature. You can have more than one: for example, you might have a blog.feature, members.feature, and resources.feature if your site has those areas available.

Of course, don’t confuse what Behat calls a feature—a set of tests—with the Features module that bundles and exports related functionality into a Drupal module.

For my current project, I created a global.feature file that checks if the blocks I expect to have in my header and footer are present. The contents of that file are:

Feature: Global Elements

  Scenario: Homepage Contact Us Link
    Given I am on the homepage
    Then I should see the link "Contact Us" in the "branding_second" region
    Then I should see the "Search" button in the "branding_second" region
    Then I should see the "div#block-system-main-menu" element in the "menu" region

As you can see, the tests is very readable even though it isn’t purely parsing natural language. Indents help organize Scenarios (a group of tests) and the conditions needed for each scenario to pass.

You can set up some conditions for the test, starting with “Given”. In this case, given that we’re on the homepage. The Drupal Extension adds ways to specify that you are a specific user, or have a specific role, and more.

Next, we list what we expect to see on the webpage. You can also tell Behat to interact with the page by specifying a link to click, form field to fill out, or a button to press. Again here, the Drupal extension (by extending the MinkExtension), provides ways to test if a link or button are in one of our configured regions. The third test above uses a CSS selector, like in jQuery, to check that the main menu block is in the menu region.

Testing user authentication

If you’re testing a site that is not local, you can use the drush api driver to test user authentication, node creation, and more. First, setup a drush alias for your site (in this example, I’m using local.dev. Then add the following are in your behat.yml:

      api_driver: 'drush'
      drush:
        alias: "local.dev"

You can then create a scenario to test the user login’s work without having to specify a test username or password by tagging them with @api

  @api
  Scenario: Admin login
    Given I am on the homepage
    Given I am logged in as a user with the "admin" role
    Then I should see the heading "Welcome" in the "content" region

If you’ve customized the username text for login, your test will fail. Don’t worry! Just add the following to your behat.yml file so that the test knows what text to look for. In this case, the username field label is just E-mail.

      text:
        username_field: "E-mail"

Custom Testing by Extending Contexts

When you initialized Behat, it created a features/bootstraps/FeatureContext.php file. This can be a handy class for writing custom tests for unique features on your site. You can add custom tests by using the Drupal Extension’s own sub-contexts. I changed my Feature Context to extend the Mink Context like this:

class FeatureContext extends MinkContext implements SnippetAcceptingContext {

Note that if you do that, you’ll need to remove MinkContext from the explicit list of default context in behat.yml.

No matter how you organize them, you can then write custom tests as methods. For example, the following will test that a link appears in the breadcrumb trail of a page. You can use CSS selectors to find items on the page, such as the ‘#breadcrumb’ div in a theme. You can also re-use other tests defined by the MinkContext like findLink.

/**
 * @Then I should see the breadcrumb link :arg1
*/
public function iShouldSeeTheBreadcrumbLink($arg1)
{
   // get the breadcrumb
   /**
     * @var Behat\Mink\Element\NodeElement $breadcrumb
     */
   $breadcrumb = $this->getSession()->getPage()->find('css', 'div#breadcrumb');

   // this does not work for URLs
   $link = $breadcrumb->findLink($arg1);
   if ($link) {
      return;
   }

   // filter by url
   $link = $breadcrumb->findAll('css', "a[href=\"{$arg1}\"]");
   if ($link) {
      return;
   }

   throw new \Exception(
       sprintf("Expected link %s not found in breadcrumb on page %s",
           $arg1,
           $this->getSession()->getCurrentUrl())
    );
}

If your context implements the SnippetAwareContext, behat will generate the Docblock and method signature when it encounters an unknown test. If you’re feature has the following:

    Then I should see "foo-logo.png" as the header logo.

When you run your tests, behat will output the error message below that you can copy and paste to your context. Anything in quotes becomes a parameter. The DocBlock contains the annotation Behat uses to find your test when it’s used in a scenario.

/**
* @Then I should see :arg1 as the header logo.
*/
public function iShouldSeeAsTheHeaderLogo($arg1)
{
   throw new PendingException();
}

Selenium

Follow the Behat docs to install selenium: http://mink.behat.org/en/latest/drivers/selenium2.html. When you’re testing you’ll need to have it running via:

java -jar /path/to/selenium-server-standalone-2.53.0.jar 

To tell Behat how to use selenium your behat.yml file should have:

      selenium2:
        wd_host: http://local.dev:4444/wd/hub
        capabilities: {"browser": "firefox"}

You’ll also need to have Firefox installed. OF course, at the time of this writing, Firefox is asking people to transition from use Webdriver to Marionette for automating browser usage. I have Firefox 47 and it’s still working with Webdriver as far as I can tell. I have not found clear, concise instructions for using Marionette with Selenium. Another option is to use Phantom.JS instead of Selenium for any features that need a real browser.

Once everything is working—you’ll know it locally because a Firefox instance will pop up—you can create a scenario like the following one. Use the @javascript tag to tell Behat to use Selenium to test it.

  @javascript
  Scenario: Modal Popup on click
    Given I am at "/some/page"
    When I click "View More Details"
    Then I wait for AJAX to finish
    Then I should see an "#modal-content" element
    Then I should see text matching "This is a Modal"

Conclusion

If you don’t have tests for your site, I urge you to push for adding them as part of your ongoing work. I’ve slowly added them to my main Drupal client project over the last few months and it’s really started to pay off. For one, I’ve captured many requirements and expectations about how pages on the site work that were only in my or the project manager’s heads, if not lost in a closed ticket somewhere. Second, whenever I merge new work in and before any deploy I can run tests. If they are all green, I can be confident that new code and bug fixes haven’t caused a regression. At the same time, I now have a way to test the site that makes it less risky to re-factor or reorganize code. I didn’t spend a lot of time building tests, but as I work on a new feature or fix a bug, writing a test is now just part of confirming that everything works as expected. For complicated features, it’s also become a time saver to have a test that automates a complicated interactions—like testing a 3-page web form, since Behat can run that scenario much faster than I can manually.

The benefits from investing in automated testing outweigh any initial cost in time and effort to set them up. What are you waiting for?


Oscar still remembers downloading an early version of the Apache HTTP server at the end of 1995, and promptly asking "Ok, what's this good for?" He started learning PHP in 2000 and hasn't stopped since. He's worked with Drupal, WordPress, Zend Framework, and bespoke PHP, to name a few. Follow him on Google+.
Tags: , , , ,
 

Responses and Pingbacks

[…] the php[architect] site there’s a new tutorial posted from Oscar Merida about testing Drupal sites with Behat, a popular PHP-based “Behavior Driven Development” testing tool to help ensure your […]

Hi,
Very nice article, but what I was expecting was more something like this http://stakeholderwhisperer.com/posts/2014/10/introducing-modelling-by-example, and not just ui testing of drupal.
Maybe, in the next one 🙂

Thanks for the link. I definitely came at this from a need to test an existing Drupal UI, not from a “pure” BDD approach since the project was already built. The ubiquitous language is interesting and I’ll do some more reading about it. At the moment, I’m not sure it makes sense to build such custom methods for testing something unlikely to change.

[…] Testing Your Drupal Site with Behat […]

I was struggling to find out why Behat did not recognize “I am on the homepage” command. After searching for a while, I found out that drupal-extension ships with a default behat.yml file (named behat.yml.dist) that lists more contexts. Maybe you should recommend in ‘Configuring Behat’ to start with that distributed file.

Leave a comment

Use the form below to leave a comment: