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
- 4,504