Basic behavior testing with Behat in Drupal

Posted June 8th, 2014

How many times have you faced this?

Scenario 1: The “okay, try now” loop

Developer: Hey manager, I just completed tickets 2, 3, 4, and 5. I am moving on to 6, 7 and 8 now.
Manager: Cool, thanks Developer. Hey wait, how come when I log in I can’t edit content?
Developer: Ohhh ahhhh one second
*… 15 minutes later … *
Developer: Try now
Manager: Okay yeah, I can see the edit links now.
Developer: Great!
Manager: I just created some test content though and I can’t delete it.. I need to get rid of it before I train the client!
Developer: Ohh, one second…..

Scenario 2: Stepping on each others toes

Developer 1: So, right now in the project we are in a good position to- hey, why isn’t this working? Where is Menu X? WTF BROKE!
Developer 2: What’s up? Yeah I made some changes for the work I was doing. Something wrong?
Developer 1: All the work I did last week isn’t working anymore!
Developer 2: It’s not? Hmm.. Are you sure?
Developer 1: omgwtfbbq!
Developer 2: …… One second..

I’ve been in both!

Yes, we all have at some point or another, and more frequently than we think. It’s frustrating, common, and the wasted minutes and/or hours not only eat into the current project budget, but hurt long term numbers too on the books.

Not only that, but the second you start having to redo work, developer morale begins a downward slide. Once that starts to happen, the overall health of the project can be in jeopardy, because with mounting tasks to redo work that was already completed, the people in charge of executing the work are not happy.

We can stop that.

Behavior testing with Behat

Super. Building on the example test in the previous post, let’s look at ways we can prevent the scenarios above.

Here was our example test. Let’s recap:


Feature: Content Management
  When I log into the website
  As an administrator
  I should be able to create, edit, and delete page content

  Scenario: An administrative user should be able create page content
    Given I am logged in as a user with the "administrator" role
    When I go to "node/add/page"
    Then I should not see "Access denied"

A basic Behat test lives inside a .feature file in a features folder. This folder is created when you first run vendor/bin/behat --init in your project.

I like to structure feature tests as follows:


features
  bootstrap
  content
    admin.feature
    editor.feature
  commerce
    anon.feature
    member.feature
  ...

For each epic a project contains, I create a folder. Within the epic are defined user stories per role, which I like to group into each feature test. Therefore, each filename is typically rolename.feature per feature name, which makes it pretty clear to me.

So, for the test example above, that would be in features/content/admin.feature. It’s up to you how you want to organize your tests, but this is the way I currently prefer to do it.

Every .feature starts out with the declaration or user story, the experience we are tasked with implementing. It’s described almost verbatim from the user stories in the project.

Scenarios

Okay. So now we have our user story defined, and our feature test created and waiting. Our project manager has asked that administrators should be able to create, edit, and delete page content.

Scenarios are individual instances to test pieces of the feature that make up the whole and in the end let us know that our feature is working correctly.

We have one above, but we need a few more to check on editing and deleting.


Feature: Content Management
  When I log into the website
  As an administrator
  I should be able to create, edit, and delete page content

  Scenario: An administrative user should be able create page content
    Given I am logged in as a user with the "administrator" role
    When I go to "node/add/page"
    Then I should not see "Access denied"

  Scenario: An administrator should be able to edit page content
    Given "page" nodes:
      | title      | body          | status  |
      | Test page  | test content  | 1       |
    When I go to "admin/content"
    And I click "edit" in the "Test page" row
    Then I should not see "Access denied"

  Scenario: An administrator should be able to delete page content
    Given "page" nodes:
      | title      | body          | status  |
      | Test page  | test content  | 1       |
    When I go to "admin/content"
    And I click "delete" in the "Test page" row
    Then I should not see "Access denied"

Running this test is simple, and backs up the integrity of your system be it permissions, interfaces or input forms.

Step Definitions

A step definition is a written statement that Behat maps to a PHP method. Everything above starting with Given, When, Then are step definitions. Behat matches them with regular expressions to know which PHP method to fire for that step.

With the Drupal Behat Extension, many are provided for you out of the box so you don’t have to make them. If you type vendor/bin/behat -dl in terminal, Behat will list all the available step definitions.

Here they are:


Given /^(?:that I|I) am at "(?P[^"]*)"$/
 When /^I visit "(?P[^"]*)"$/
 When /^I click "(?P<link>[^"]*)"$/
Given /^for "(?P<field>[^"]*)" I enter "(?P<value>[^"]*)"$/
Given /^I enter "(?P<value>[^"]*)" for "(?P<field>[^"]*)"$/
Given /^I wait for AJAX to finish$/
 When /^(?:|I )press "(?P<button>(?:[^"]|\\")*)"$/
 When /^(?:|I )press the "(?P<button>[^"]*)" button$/
Given /^(?:|I )press the "([^"]*)" key in the "([^"]*)" field$/
 Then /^I should see the link "(?P<link>[^"]*)"$/
 Then /^I should not see the link "(?P<link>[^"]*)"$/
 Then /^I (?:|should )see the heading "(?P<heading>[^"]*)"$/
 Then /^I (?:|should )not see the heading "(?P<heading>[^"]*)"$/
 Then /^I should see the heading "(?P<heading>[^"]*)" in the "(?P<region>[^"]*)"(?:| region)$/
 Then /^I should see the "(?P<heading>[^"]*)" heading in the "(?P<region>[^"]*)"(?:| region)$/
 When /^I (?:follow|click) "(?P<link>[^"]*)" in the "(?P<region>[^"]*)"(?:| region)$/
 Then /^I should see the link "(?P<link>[^"]*)" in the "(?P<region>[^"]*)"(?:| region)$/
 Then /^I should not see the link "(?P<link>[^"]*)" in the "(?P<region>[^"]*)"(?:| region)$/
 Then /^I should see (?:the text |)"(?P<text>[^"]*)" in the "(?P<region>[^"]*)"(?:| region)$/
 Then /^I should not see (?:the text |)"(?P<text>[^"]*)" in the "(?P<region>[^"]*)"(?:| region)$/
Given /^I press "(?P<button>[^"]*)" in the "(?P<region>[^"]*)"(?:| region)$/
Given /^(?:|I )fill in "(?P<value>(?:[^"]|\\")*)" for "(?P<field>(?:[^"]|\\")*)" in the "(?P<region>[^"]*)"(?:| region)$/
Given /^(?:|I )fill in "(?P<field>(?:[^"]|\\")*)" with "(?P<value>(?:[^"]|\\")*)" in the "(?P<region>[^"]*)"(?:| region)$/
 Then /^(?:I|I should) see the text "(?P<text>[^"]*)"$/
 Then /^I should not see the text "(?P<text>[^"]*)"$/
 Then /^I should get a "(?P<code>[^"]*)" HTTP response$/
 Then /^I should not get a "(?P<code>[^"]*)" HTTP response$/
Given /^I check the box "(?P<checkbox>[^"]*)"$/
Given /^I uncheck the box "(?P<checkbox>[^"]*)"$/
 When /^I select the radio button "(?P<label>[^"]*)" with the id "(?P<id>[^"]*)"$/
 When /^I select the radio button "(?P<label>[^"]*)"$/
Given /^I am an anonymous user$/
Given /^I am not logged in$/
Given /^I am logged in as a user with the "(?P<role>[^"]*)" role$/
Given /^I am logged in as "(?P<name>[^"]*)"$/
Given /^I am logged in as a user with the "(?P<permission>[^"]*)" permission(?:|s)$/
Given /^I click "(?P<link>[^"]*)" in the "(?P<row_text>[^"]*)" row$/
Given /^the cache has been cleared$/
Given /^I run cron$/
Given /^I am viewing (?:a|an) "(?P<type>[^"]*)" node with the title "(?P<title>[^"]*)"$/
Given /^(?:a|an) "(?P<type>[^"]*)" node with the title "(?P<title>[^"]*)"$/
Given /^I am viewing my "(?P<type>[^"]*)" node with the title "(?P<title>[^"]*)"$/
Given /^"(?P<type>[^"]*)" nodes:$/
Given /^I am viewing (?:a|an) "(?P<type>[^"]*)" node:$/
 Then /^I should be able to edit (?:a|an) "([^"]*)" node$/
Given /^I am viewing (?:a|an) "(?P<vocabulary>[^"]*)" term with the name "(?P<name>[^"]*)"$/
Given /^(?:a|an) "(?P<vocabulary>[^"]*)" term with the name "(?P<name>[^"]*)"$/
Given /^users:$/
Given /^"(?P<vocabulary>[^"]*)" terms:$/
 Then /^I should see the error message(?:| containing) "([^"]*)"$/
 Then /^I should see the following <error messages>$/
Given /^I should not see the error message(?:| containing) "([^"]*)"$/
 Then /^I should not see the following <error messages>$/
 Then /^I should see the success message(?:| containing) "([^"]*)"$/
 Then /^I should see the following <success messages>$/
Given /^I should not see the success message(?:| containing) "([^"]*)"$/
 Then /^I should not see the following <success messages>$/
 Then /^I should see the warning message(?:| containing) "([^"]*)"$/
 Then /^I should see the following <warning messages>$/
Given /^I should not see the warning message(?:| containing) "([^"]*)"$/
 Then /^I should not see the following <warning messages>$/
 Then /^I should see the message(?:| containing) "([^"]*)"$/
 Then /^I should not see the message(?:| containing) "([^"]*)"$/
Given /^I run drush "(?P<command>[^"]*)"$/
Given /^I run drush "(?P<command>[^"]*)" "(?P<arguments>(?:[^"]|\\")*)"$/
 Then /^drush output should contain "(?P<output>[^"]*)"$/
 Then /^drush output should not contain "(?P<output>[^"]*)"$/
 Then /^(?:|I )break$/
Given /^(?:|I )am on (?:|the )homepage$/
 When /^(?:|I )go to (?:|the )homepage$/
Given /^(?:|I )am on "(?P<page>[^"]+)"$/
 When /^(?:|I )go to "(?P<page>[^"]+)"$/
 When /^(?:|I )reload the page$/
 When /^(?:|I )move backward one page$/
 When /^(?:|I )move forward one page$/
 When /^(?:|I )follow "(?P<link>(?:[^"]|\\")*)"$/
 When /^(?:|I )fill in "(?P<field>(?:[^"]|\\")*)" with "(?P<value>(?:[^"]|\\")*)"$/
 When /^(?:|I )fill in "(?P<field>(?:[^"]|\\")*)" with:$/
 When /^(?:|I )fill in "(?P<value>(?:[^"]|\\")*)" for "(?P<field>(?:[^"]|\\")*)"$/
 When /^(?:|I )fill in the following:$/
 When /^(?:|I )select "(?P<option>(?:[^"]|\\")*)" from "(?P<select>(?:[^"]|\\")*)"$/
 When /^(?:|I )additionally select "(?P<option>(?:[^"]|\\")*)" from "(?P<select>(?:[^"]|\\")*)"$/
 When /^(?:|I )check "(?P<option>(?:[^"]|\\")*)"$/
 When /^(?:|I )uncheck "(?P<option>(?:[^"]|\\")*)"$/
 When /^(?:|I )attach the file "(?P[^"]*)" to "(?P<field>(?:[^"]|\\")*)"$/
 Then /^(?:|I )should be on "(?P<page>[^"]+)"$/
 Then /^(?:|I )should be on (?:|the )homepage$/
 Then /^the (?i)url(?-i) should match (?P<pattern>"(?:[^"]|\\")*")$/
 Then /^the response status code should be (?P<code>\d+)$/
 Then /^the response status code should not be (?P<code>\d+)$/
 Then /^(?:|I )should see "(?P<text>(?:[^"]|\\")*)"$/
 Then /^(?:|I )should not see "(?P<text>(?:[^"]|\\")*)"$/
 Then /^(?:|I )should see text matching (?P<pattern>"(?:[^"]|\\")*")$/
 Then /^(?:|I )should not see text matching (?P<pattern>"(?:[^"]|\\")*")$/
 Then /^the response should contain "(?P<text>(?:[^"]|\\")*)"$/
 Then /^the response should not contain "(?P<text>(?:[^"]|\\")*)"$/
 Then /^(?:|I )should see "(?P<text>(?:[^"]|\\")*)" in the "(?P<element>[^"]*)" element$/
 Then /^(?:|I )should not see "(?P<text>(?:[^"]|\\")*)" in the "(?P<element>[^"]*)" element$/
 Then /^the "(?P<element>[^"]*)" element should contain "(?P<value>(?:[^"]|\\")*)"$/
 Then /^the "(?P<element>[^"]*)" element should not contain "(?P<value>(?:[^"]|\\")*)"$/
 Then /^(?:|I )should see an? "(?P<element>[^"]*)" element$/
 Then /^(?:|I )should not see an? "(?P<element>[^"]*)" element$/
 Then /^the "(?P<field>(?:[^"]|\\")*)" field should contain "(?P<value>(?:[^"]|\\")*)"$/
 Then /^the "(?P<field>(?:[^"]|\\")*)" field should not contain "(?P<value>(?:[^"]|\\")*)"$/
 Then /^the "(?P<checkbox>(?:[^"]|\\")*)" checkbox should be checked$/
 Then /^the checkbox "(?P<checkbox>(?:[^"]|\\")*)" (?:is|should be) checked$/
 Then /^the "(?P<checkbox>(?:[^"]|\\")*)" checkbox should not be checked$/
 Then /^the checkbox "(?P<checkbox>(?:[^"]|\\")*)" should (?:be unchecked|not be checked)$/
 Then /^the checkbox "(?P<checkbox>(?:[^"]|\\")*)" is (?:unchecked|not checked)$/
 Then /^(?:|I )should see (?P<num>\d+) "(?P<element>[^"]*)" elements?$/
 Then /^print current URL$/
 Then /^print last response$/
 Then /^show last response$/

Holy moly. We have lots of viable definitions to use to write tests with.

Each one of these are usable just the way they are written. For instance, if you wanted to check for “Welcome!” on the homepage, there are two steps provided that can do that already, Given /^(?:|I )am on (?:|the )homepage$/ and Then /^(?:I|I should) see the text "(?P<text>[^"]\*)"$/. Here’s how:


  Scenario: A user should see "Welcome!" on the homepage
    Given I am on the homepage
    Then I should see the text "Welcome!"

Nothing to it, right? That’s the entire test. Behat matches statements with regular expressions and passes quoted text as arguments to their respective step definitions.

How does this black magic work?

Step definitions like this are already provided to you out of the box. They are defined in their respective providing classes, and Behat matches the statements back to annotated code comments preceding the PHP methods that execute them. Here is Given /^(?:|I )am on (?:|the )homepage$/:


/**
 * Opens homepage.
 *
 * @Given /^(?:|I )am on (?:|the )homepage$/
 * @When /^(?:|I )go to (?:|the )homepage$/
 */
public function iAmOnHomepage()
{
    $this->getSession()->visit($this->locatePath('/'));
}

When the PHP method is used, the Behat driver (the mechanism that simulates client / browser interaction) directs the client to “/” or the root document page of the site. Make sense now?

Here’s Then /^(?:I|I should) see the text "(?P<text>[^"]\*)"$/:


/**
 * @Then /^(?:I|I should) see the text "(?P<text>[^"]\*)"$/
 */
public function assertTextVisible($text) {
  // Use the Mink Extension step definition.
  return new Given("I should see text matching \"$text\"");
}

An argument passed to this function, which is the quoted string "Welcome!". If you followed the class, you can see that this step is generated dynamically for Mink, and the statement is then evaluated against the page contents.

Before we get deeper into step definitions, backgrounds, contexts, Selenium, and region selectors, take some time to build basic tests with the list of step definitions above. Get them to pass and get them to fail. Build confidence in the fact that Behat is your second set of eyes and a helper in facilitating feature development.