Story BDD with Behat

BDD

Beer Driven Development

TODO

Behaviour Driven Development

The Problem

How the customer explained it.

How the project leader understood it.

How the programmer wrote it.

What the beta testers received.

What the customer really needed.

The Solution

Let's all use same vocabulary

  • Project managers
  • Developers
  • Business analysts
  • Product owners
  • Testers
  • ...

...for planning, implementing and testing feature

  • Project managers
  • Developers
  • Business analysts
  • Product owners
  • Testers
  • ...

...with the focus on the behavior of the feature

  • Project managers
  • Developers
  • Business analysts
  • Product owners
  • Testers
  • ...

StoryBDD helps ensuring that development team has understanding of business on the same level that client does.

And even leveling up clients knowledge

of his own business.

But how?

By forcing you to answer business questions.

  • Who?

  • What?

  • How?

  • Who benefits? 

  • The business value.

  • Feature description.

Gherkin

Gherkin

A structured language to describe a feature.

Feature: News admin panel
    In order to maintain a list of news
    As a site administrator
    I need to be able to edit news

Feature: News admin panel
    In order to maintain a list of news
    As a site administrator
    I need to be able to edit news

Feature: (custom title)
    In order to (the business value)
    As a (the person who benefits)
    I need (feature description)

Feature: News admin panel
    In order to maintain a list of news
    As a site administrator
    I need to be able to edit news

 

    Scenario: Add new article
         Given I am on the "/admin/news" page
         When I click "New Article"
            And I fill in "Title" with "Learn BDD"
            And I press "Save"
          Then I should see "A new article was added"

Feature: News admin panel
    In order to maintain a list of news
    As a site administrator
    I need to be able to edit news

 

    Scenario: Add new article
         Given I am on the "/admin/news" page
         When I click "New Article"
            And I fill in "Title" with "Learn BDD"
            And I press "Save"

          Then I should see "A new article was added"

Feature: News admin panel
    In order to maintain a list of news
    As a site administrator
    I need to be able to edit news

 

    Scenario: (scenario title)
         Given (initial state of the system)
         When (action taken by the person/role)
            And (can be added to create multiple...)
            And (...Given/When/Then lines)
          Then (observable system state after the action has been performed)

Gherkin

Consistent language to describe features and their scenarios.

Behat

Imagine that we can execute Gherkin sentences as functional tests.

Behat

Maps each step (Gherkin sentence) to a PHP callback.

Installing Behat

{
    "require": {
        "behat/behat": "~3.0"
    }
}

CLI application

$ ./bin/behat --help
Usage:
 behat [-s|--suite="..."] [-f|--format="..."] [-o|--out="..."] [--format-settings="..."] [--init] [--lang="..."] [--name="..."] [--tags="..."] [--role="..."] [--story-syntax] [-d|--definitions="..."] [--append-snippets] [--no-snippets] [--strict] [--rerun] [--stop-on-failure] [--dry-run] [paths]

Arguments:
 paths                 Optional path(s) to execute. Could be:
                       - a Symfony2 bundle path (@BundleName/)
                       - a dir (features/)
                       - a feature (*.feature)
                       - a scenario at specific line (*.feature:10).
                       - all scenarios at or after a specific line (*.feature:10-*).
                       - all scenarios at a line within a specific range (*.feature:10-20).
                       - a scenarios list file (*.scenarios).

Options:
 --suite (-s)          Only execute a specific suite.
 --format (-f)         How to format tests output. pretty is default.
                       Available formats are:
                       - progress: Prints one character per step.
                       - pretty: Prints the feature as is.
                       You can use multiple formats at the same time. (multiple values allowed)
 --out (-o)            Write format output to a file/directory instead of
                       STDOUT (output_path). You can also provide different
                       outputs to multiple formats. (multiple values allowed)
 --format-settings     Set formatters parameters using json object.
                       Keys are parameter names, values are values. (multiple values allowed)
 --init                Initialize all registered test suites.
 --lang                Print output in particular language.
 --name                Only executeCall the feature elements which match part
                       of the given name or regex. (multiple values allowed)
 --tags                Only executeCall the features or scenarios with tags
                       matching tag filter expression. (multiple values allowed)
 --role                Only executeCall the features with actor role matching
                       a wildcard.
 --story-syntax        Print *.feature example.
                       Use --lang to see specific language.
 --definitions (-d)    Print all available step definitions:
                       - use --definitions l to just list definition expressions.
                       - use --definitions i to show definitions with extended info.
                       - use --definitions 'needle' to find specific definitions.
                       Use --lang to see definitions in specific language.
 --append-snippets     Appends snippets for undefined steps into main context.
 --no-snippets         Do not print snippets for undefined steps after stats.
 --strict              Passes only if all tests are explicitly passing.
 --rerun               Re-run scenarios that failed during last execution.
 --stop-on-failure     Stop processing on first failed scenario.
 --dry-run             Invokes formatters without executing the tests and hooks.
 --profile (-p)        Specify config profile to use.
 --config (-c)         Specify config file to use.
 --verbose (-v)        Increase verbosity of exceptions.
 --help (-h)           Display this help message.
 --config-reference    Display the configuration reference.
 --version (-V)        Display this behat version.
 --no-interaction (-n) Do not ask any interactive question.
 --colors              Force ANSI color in the output. By default color support is
                       guessed based on your platform and the output if not specified.
 --no-colors           Force no ANSI color in the output.

Initialize

$ php bin/behat --init
+d features - place your *.feature files here
+d features/bootstrap - place your context classes here
+f features/bootstrap/FeatureContext.php - place your definitions, transformations and hooks here

Behat needs:

  • *.feature files to be executed.
  • FeatureContext.php to hold PHP callbacks.
  • Optional behat.yml configuration.

Linus Torvalds didn't write Linux using BDD

But we can

Feature: ls
    In order to see the directory structure
    As a UNIX user
    I need to be able to list directory's contents

features/ls.feature

Scenario

If you have two files in a directory, and you run ls command - you should see them listed.

Scenario: List 2 files in a directory
    Given I have a file named "foo"
       And I have a file named "bar"
    When I run "ls"
     Then I should see "foo" in the output
       And I should see "bar" in the output

features/ls.feature

$ php bin/behat

$ php bin/behat 
Feature: ls
  In order to see the directory structure
  As a UNIX user
  I need to be able to list directory's contents

  Scenario: List 2 files in a directory   # features/ls.feature:6
    Given I have a file named "foo"
    And I have a file named "bar"
    When I run "ls"
    Then I should see "foo" in the output
    And I should see "bar" in the output

1 scenario (1 undefined)
5 steps (5 undefined)
0m0.01s (10.10Mb)

--- FeatureContext has missing steps. Define them with these snippets:

    /**
     * @Given I have a file named :arg1
     */
    public function iHaveAFileNamed($arg1)
    {
        throw new PendingException();
    }

    /**
     * @When I run :arg1
     */
    public function iRun($arg1)
    {
        throw new PendingException();
    }

    /**
     * @Then I should see :arg1 in the output
     */
    public function iShouldSeeInTheOutput($arg1)
    {
        throw new PendingException();
    }

Implement generated callbacks

/**
 * @Given I have a file named :file
 */
public function iHaveAFileNamed($file)
{
    touch($file);
}

/**
 * @When I run :command
 */
public function iRun($command)
{
    $this->output = shell_exec($command);
}

$ php bin/behat

$ php bin/behat
Feature: ls
  In order to see the directory structure
  As a UNIX user
  I need to be able to list directory's contents

  Scenario: List 2 files in a directory   # features/ls.feature:6
    Given I have a file named "foo"       # FeatureContext::iHaveAFileNamed()
    And I have a file named "bar"         # FeatureContext::iHaveAFileNamed()
    When I run "ls"                       # FeatureContext::iRun()
    Then I should see "foo" in the output # FeatureContext::iShouldSeeInTheOutput()
    And I should see "bar" in the output  # FeatureContext::iShouldSeeInTheOutput()

1 scenario (1 passed)
5 steps (5 passed)
0m0.02s (10.22Mb)

We can map each step to a PHP callback with Behat.

Mink

Mink

Mink is an open source browser controller/emulator for web applications.

One API

  • Goutte
  • Selenium
  • Zombie.js
  • ...

Using Mink

use Behat\Mink\Session;
use Behat\Mink\Driver\GoutteDriver;

$session = new Session(new GoutteDriver());

$session->visit($startUrl);
$session->getPage()->findLink('Downloads')->click();

echo 'Status: '.$session->getPage()->getStatusCode();
echo 'Content: '.$session->getPage()->getContent();

Mink inside FeatureContext

Mink Extension

Easy to use Mink inside FeatureContext.

Update dependencies

{
    "require": {
        "behat/behat": "~3.0",
        "behat/mink-extension": "~2.0@dev",
        "behat/mink-goutte-driver": "~1.0",
        "behat/mink-selenium2-driver": "~1.1"
    }
}

Mink inside FeatureContext

namespace Hypebeast\Bundle\WebBundle\Behat;

use Behat\MinkExtension\Context\RawMinkContext;

class FeatureContext extends RawMinkContext
{
    public function doSomething()
    {
        $this->getSession()->visit('https://github.com/');
    }
}

FeatureContext extends MinkContext

namespace Hypebeast\Bundle\WebBundle\Behat;

use Behat\MinkExtension\Context\RawMinkContext;

class FeatureContext extends MinkContext
{
    public function doSomething()
    {
        $this->visit('/foo');
        $this->selectOption('Country', 'Poland');
        $this->pressButton('Save');

        $this->assertPageContainsText('Country changed.');
    }
}

After adding MinkContext

$ php bin/behat -dl
default | Given /^(?:|I )am on (?:|the )homepage$/
default |  When /^(?:|I )go to (?:|the )homepage$/
default | Given /^(?:|I )am on "(?P<page>[^"]+)"$/
default |  When /^(?:|I )go to "(?P<page>[^"]+)"$/
default |  When /^(?:|I )reload the page$/
default |  When /^(?:|I )move backward one page$/
default |  When /^(?:|I )move forward one page$/
default |  When /^(?:|I )press "(?P<button>(?:[^"]|\\")*)"$/
default |  When /^(?:|I )follow "(?P<link>(?:[^"]|\\")*)"$/
default |  When /^(?:|I )fill in "(?P<field>(?:[^"]|\\")*)" with "(?P<value>(?:[^"]|\\")*)"$/
default |  When /^(?:|I )fill in "(?P<field>(?:[^"]|\\")*)" with:$/
default |  When /^(?:|I )fill in "(?P<value>(?:[^"]|\\")*)" for "(?P<field>(?:[^"]|\\")*)"$/
default |  When /^(?:|I )fill in the following:$/
default |  When /^(?:|I )select "(?P<option>(?:[^"]|\\")*)" from "(?P<select>(?:[^"]|\\")*)"$/
default |  When /^(?:|I )additionally select "(?P<option>(?:[^"]|\\")*)" from "(?P<select>(?:[^"]|\\")*)"$/
default |  When /^(?:|I )check "(?P<option>(?:[^"]|\\")*)"$/
default |  When /^(?:|I )uncheck "(?P<option>(?:[^"]|\\")*)"$/
default |  When /^(?:|I )attach the file "(?P<path>[^"]*)" to "(?P<field>(?:[^"]|\\")*)"$/
default |  Then /^(?:|I )should be on "(?P<page>[^"]+)"$/
default |  Then /^(?:|I )should be on (?:|the )homepage$/
default |  Then /^the (?i)url(?-i) should match (?P<pattern>"(?:[^"]|\\")*")$/
default |  Then /^the response status code should be (?P<code>\d+)$/
default |  Then /^the response status code should not be (?P<code>\d+)$/
default |  Then /^(?:|I )should see "(?P<text>(?:[^"]|\\")*)"$/
default |  Then /^(?:|I )should not see "(?P<text>(?:[^"]|\\")*)"$/
default |  Then /^(?:|I )should see text matching (?P<pattern>"(?:[^"]|\\")*")$/
default |  Then /^(?:|I )should not see text matching (?P<pattern>"(?:[^"]|\\")*")$/
default |  Then /^the response should contain "(?P<text>(?:[^"]|\\")*)"$/
default |  Then /^the response should not contain "(?P<text>(?:[^"]|\\")*)"$/
default |  Then /^(?:|I )should see "(?P<text>(?:[^"]|\\")*)" in the "(?P<element>[^"]*)" element$/
default |  Then /^(?:|I )should not see "(?P<text>(?:[^"]|\\")*)" in the "(?P<element>[^"]*)" element$/
default |  Then /^the "(?P<element>[^"]*)" element should contain "(?P<value>(?:[^"]|\\")*)"$/
default |  Then /^the "(?P<element>[^"]*)" element should not contain "(?P<value>(?:[^"]|\\")*)"$/
default |  Then /^(?:|I )should see an? "(?P<element>[^"]*)" element$/
default |  Then /^(?:|I )should not see an? "(?P<element>[^"]*)" element$/
default |  Then /^the "(?P<field>(?:[^"]|\\")*)" field should contain "(?P<value>(?:[^"]|\\")*)"$/
default |  Then /^the "(?P<field>(?:[^"]|\\")*)" field should not contain "(?P<value>(?:[^"]|\\")*)"$/
default |  Then /^the "(?P<checkbox>(?:[^"]|\\")*)" checkbox should be checked$/
default |  Then /^the checkbox "(?P<checkbox>(?:[^"]|\\")*)" (?:is|should be) checked$/
default |  Then /^the "(?P<checkbox>(?:[^"]|\\")*)" checkbox should not be checked$/
default |  Then /^the checkbox "(?P<checkbox>(?:[^"]|\\")*)" should (?:be unchecked|not be checked)$/
default |  Then /^the checkbox "(?P<checkbox>(?:[^"]|\\")*)" is (?:unchecked|not checked)$/
default |  Then /^(?:|I )should see (?P<num>\d+) "(?P<element>[^"]*)" elements?$/
default |  Then /^print current URL$/
default |  Then /^print last response$/
default |  Then /^show last response$/

Which means

We can write functional tests without writing a single line of PHP code!

Feature: Search
    In order to find articles
    As a website visitor
    I need to be able to search for articles

    Scenario: Searching for an article that exists
        Given I am on the homepage
         When I fill in "search" with "BDD"
          And I press "go"
          And I follow "Behavior-driven development"
         Then I should see "History"
          And I should see "Principles of BDD"
          And I should see "Story versus specification"

Story BDD with Behat

By Saša Stamenković

Story BDD with Behat

  • 3,362