An indepth dive into using Behat/Mink/Selenium for BDD testing.
* http://behat.org
* http://mink.behat.org/
* http://docs.seleniumhq.org/
In this talk I'll cover:
* why and when to use Behat (and when not)
* Installation and configuration of Behat and Mink
* Building Behat Contexts
* Avoiding data deadlocks and "test user account" syndrome
* Introduction to Selenium and testing JavaScript
* Best practises for writing tests (what to avoid, what to aspire for, writing stories like you mean it, how to get your product owners to write them)
* Common gotchas
16. Feature: Party harmony
As a leader, I want to ensure harmony and mutual trust, so that
we work as a team
Scenario: Teach members to respect others’ property
Given that the Wizard has 10 cookies
And the Bard eats 1 cookie
Then the Bard mysteriously catches fire
Cucumber Syntax
Readable testing language
17. class FeatureContext … {
/**
* @Given that the wizard has :num cookies
*/
public function wizardHasCookies($num) {
// $this->wizard is a pre-existing condition... like syphilis
$this->wizard->setNumberOfCookies($num);
}
}
and converts it into
FeatureContext.php
18. Feature: Party harmony
As a leader, I want to ensure harmony and mutual trust, so that
we work as a team
Scenario: Teach members to respect others’ property
Given that the Wizard has 10 cookies
And the Bard eats 1 cookie
Then the Bard mysteriously catches fire
Cucumber Syntax
What’s missing?
19. Scenario:
Given that the wizard has 10 cookies
And the Bard eats 1 cookie
Then the Bard mysteriously catches fire
Fire spell fizzled (OutOfManaException)
1 scenario (1 failed)
3 steps (2 passed, 1 failed)
0m0.03s (14.19Mb)
Remember {P} C {Q}
Set your starting states
20. Feature: Party harmony
As a leader, I want to ensure harmony and mutual trust, so that
we work as a team
Background:
The Wizard’s fire spell is fully charged
And the Bard is currently not on fire
Scenario: Teach members to respect others’ property
Given that the Wizard has 10 cookies
And the Bard eats 1 cookie
Then the Bard mysteriously catches fire
Remember {P} C {Q}
Set your starting states
21. ???
As a leader, I want to ensure harmony and
mutual trust, so that we work as a team
23. Front end testing is code coverage for your user stories
User stories
Coverage
Features are your contract with the stakeholders
Contract
Scenarios are the use cases that outline the user story
Scenarios
24. Legend has it...
… that someone once convinced their PO to
write all their front end tests.
25. class MinkContext … {
/**
* Clicks link with specified id|title|alt|text.
*
* @When /^(?:|I )follow "(?P<link>(?:[^"]|")*)"$/
*/
public function clickLink($link) {
$link = $this->fixStepArgument($link);
$this->getSession()->getPage()->clickLink($link);
}
}
Mink provides...
MinkContext.php
28. $ ./vendor/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
Initialize
Create a new test suite
30. $ ./vendor/bin/behat -dl
Given /^(?:|I )am on "(?P<page>[^"]+)"$/
When /^(?:|I )reload the page$/
When /^(?:|I )move backward one page$/
When /^(?:|I )move forward one page$/
When /^(?:|I )press "(?P<button>(?:[^"]|")*)"$/
When /^(?:|I )follow "(?P<link>(?:[^"]|")*)"$/
When /^(?:|I )fill in "(?P<field>(?:[^"]|")*)" with "(?P<value>(?:
[^"]|")*)"$/
Context
What does Mink bring to the table?
32. Feature: Authentication and authorisation
As a security conscious developer I wish to ensure that only
valid users can access our website.
Scenario: Attempt to login with invalid details
Given I am on "/login"
When I fill in "email" with "some@guy.com"
And I fill in "password" with "invalid"
And I press "Login"
Then I should see "Invalid Email or Password"
Our first feature
auth.feature
33. $ ./vendor/bin/behat --config behat.yml features/auth.feature
Scenario: Attempt to login with an invalid account
Given I am on "/login"
When I fill in "email" with "some@guy.com"
And I fill in "password" with "invalid"
And I press "Login"
Then I should see "Invalid Email or Password"
1 scenarios (1 passed)
5 steps (5 passed)
Victory
output
34. Feature: Authentication and authorisation
As a security conscious developer I wish to ensure that only
valid users can access our website.
Scenario: Attempt to login with invalid details
Given I login as "some@guy.com" with password "invalid"
Then I should see "Invalid Email or Password"
Simplify
auth.feature
35. Scenario: Attempt to login with an invalid account
Given I login as "bob@smith.com" with password "invalid"
Then I should see "Invalid Email or Password"
1 scenario (1 undefined)
/**
* @Given I login as :arg1 with password :arg2
*/
public function iLoginAsWithPassword($arg1, $arg2) {
throw new PendingException();
}
Simplify
output
36. class FeatureContext … {
/**
* @Given I login as :username with password :password
*/
public function iLoginAsWithPassword($username, $password) {
$this->visit("/login");
$this->fillField("email", $username);
$this->fillField("password", $password);
$this->pressButton("Login");
}
}
Simplify
FeatureContext.php
37. Scenario: Attempt to login with an invalid account
Given I login as "bob@smith.com" with password "invalid"
Then I should see "Invalid Email or Password"
1 scenarios (1 passed)
2 steps (2 passed)
Simplify
output
38. Feature: Authentication and authorisation
As a security conscious developer I wish to ensure that only
valid users can access our website.
Scenario: Attempt to register a new user
Given I am on "/signup"
When I fill in "email" with "some@guy.com"
And I fill in "password" with "valid"
And I fill in "password2" with "valid"
And I fill in "first_name" with "some"
And I fill in "last_name" with "guy"
And I press "Create my speaker profile"
Then I should see "You’ve successfully created your account"
Our first hurdle
This ones easy, you do…. oh….
40. $ composer require robmorgan/phinx="~0.4"
Phinx to the rescue
Install
$ php vendor/bin/phinx init
Phinx by Rob Morgan - https://phinx.org. version 0.4.3
Created ./phinx.xml
Configuration
$ php vendor/bin/phinx create InitialMigration
Creating
SID
E
N
O
TE
41. #!/usr/bin/env bash
DATABASE="opencfp"
mysql -e "DROP DATABASE IF EXISTS $DATABASE" -uroot -p123
mysql -e "CREATE DATABASE $DATABASE" -uroot -p123
vendor/bin/phinx migrate
vendor/bin/behat
But it’s a bit extreme
run-behat-test.sh
42. SAVEPOINT identifier;
# Run tests
ROLLBACK TO SAVEPOINT identifier;
RELEASE SAVEPOINT identifier;
Transaction/Rollback
Roll your own solution
44. # Stop the currently running service
sudo service postfix stop
# Dumps outgoing emails to file as "day.hour.minute.second"
smtp-sink -d "%d.%H.%M.%S" localhost:2500 1000 &
vendor/bin/behat
smtp-sink
run-behat-test.sh
46. class DatabaseContext {
public function __construct($dsn, $user, $pass) {
$this->dbh = new PDO($dsn, $user, $pass);
}
/**
* @When /^there is no user called :user$/
*/
public function removeUser($user) {
$this->dbh->prepare("DELETE FROM `users` WHERE username=?")
->query([$user]);
}
}
A new context
DatabaseContext.php
SID
E
N
O
TE
51. // Make sure your server and your behat client have the same time set
// Share the secret key between the two. The code should be valid for
// 30 second periods
$code = sha1($secret_key . floor(time() / 30));
if ($request->get("code") === $code) {
// Bypass captcha
}
Easier way
Simple but safe bypass
52. Our first talk
Set the stage
Feature: Manage paper submissions
In order to ensure that speakers can submit their papers
As an speaker I need to be able to manage my own submissions
Background:
There is a speaker registered as "some@guy.com" with a
password "secrets"
I login as "some@guy.com" with password "secrets"
Scenario: Add a new talk to our submissions
...
53. Our first talk
Talk submission in 3, 2, 1...
Scenario: Add a new talk to our submissions
Given I am on "talk/create"
And I fill in the following:
| title | Behat Talk |
| description | Awesome |
| type | regular |
| category | testing |
| level | mid |
And I check "desired"
And I press "Submit my talk!"
Then I should see "Success: Successfully added talk."
55. Well that won’t work
talks.feature
Feature: Manage paper submissions
In order to ensure that speakers can submit their papers
As an speaker I need to be able to manage my own submissions
Scenario: Delete a talk
Given create a talk called "Behat Talk"
And I am on "/dashboard"
When I follow "Delete"
And I should not see "Behat Talk Changed"
The text "Behat Talk Changed" appears in the text of this page,
but it should not. (BehatMinkExceptionResponseTextException)
56. // Guzzle using web scraper
behat/mink-goutte-driver
// Java-based distributed browser workers (support JavaScript)
behat/mink-selenium2-driver
behat/mink-sahi-driver
// node.js headless browser proxy (support JavaScript)
behat/mink-zombie-driver
Drivers
Some take the scenic route
58. $ java -jar selenium-server-standalone-2.*.jar
Selenium
@javascript # Or we could use @selenium2
Feature: Manage paper submissions
In order to ensure that speakers can submit their papers
As an speaker I need to be able to manage my own submissions
Start Selenium Server
Specify javascript requirement
59. $ ./vendor/bin/behat --tags speaker,talk
Tags
Run specific tags
@speaker
Feature: Manage paper submissions
In order to ensure that speakers can submit their papers
As an speaker I need to be able to manage my own submissions
@talk
Scenario: Create a new talk
Given I am logged in as a speaker ...
SID
E
N
O
TE
60. Feature: Submitting and managing talks
As a speaker I wish be able to submit talks so I can get a chance
to talk at a conference.
@javascript
Scenario: Delete a talk
Given create a talk called "Behat Talk"
And I am on "/dashboard"
When I fill "Delete"
And I accept alerts
And I should not see "Behat Talk"
Enable JavaScript
talks.feature
61. Run as JavaScript
talks.feature
Feature: Submitting and managing talks
As a speaker I wish be able to submit talks so I can get a chance
to talk at a conference.
Scenario: Delete a talk
Given create a talk called "Behat Talk"
And I am on "/dashboard"
When I follow "Delete"
And I accept alerts
And I should not see "Behat Talk"
62. class FeatureContext … {
public function takeAScreenshotCalled($filename) {
$driver = get_class($this->getSession()->getDriver());
if ($driver == 'BehatMinkDriverSelenium2Driver') {
$ss = $this->getSession()
->getDriver()
->getScreenshot();
file_put_contents($filename, $ss);
}
}
}
Screenshot
FeatureContext.php
65. class FeatureContext … {
/**
* @AfterScenarioScope
*/
public function afterScenario(AfterScenarioScope $scope) {
$scenario = $scope->getScenario()->getTitle();
$filename = make_safe_filename($scenario);
// Take a screenshot and put it on a dashboard
// where people can see it
}
}
Hooks
FeatureContext.php
66. class FeatureContext … {
/**
* @AfterStep
*/
public function afterStep(AfterStepScope $scope) {
$code = $event->getTestResult()->getResultCode();
if ($code == TestResult::FAILED) {
// Take a screenshot
}
}
}
Hooks
FeatureContext.php
75. class MemoryContext {
/**
* @Transform /^memory:(.*)$/
*/
public function fromMemory($key) {
if (!isset($this->memory[$key])) {
throw new LogicException("Entry $key does not exist");
}
return $this->memory[$key];
}
}
Transformations
MemoryContext.php
78. $ ./vendor/bin/behat --suite admin
Suites
Run a specific suite
Feature: Managing the CFP
In order to ensure that speakers can submit their papers
As an admin
I need to be able to open the call for papers
79. $ ./vendor/bin/behat --suite speaker
Suites
Run a specific suite
@speaker
Feature: Submitting to the CFP
In order to ensure that the conference has papers
As an speaker
I need to be able to submit papers