Booking.com
W
E
AR
E
H
IR
IN
G
Work @ Booking: http://grnh.se/seomt7
I put on my
mink and
wizard behat
Questing in the world of
front end testing
Hoare Logic
{P} C {Q}
Hodor Logic
{P} C {Q}
Why?
What's the benefit?
Meet The Party
Don’t feed the druid after midnight
Task
Each test has a different approach
Barbarian
Quality Assurance
Ranger
Unit Test
Cleric
Continuous Integration
Wizard
Front End Test
Dreaded Bugbear
Teamwork
Wizards are squishy
The glue
Behat
(cucumber syntax)
Mink
(browser emulation)
Goutte
(web driver)
Selenium
(web driver)
Zombie
(web driver)
Guzzle
(curl)
Selenium RC
(java)
Zombie.js
(node.js)
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 is mysteriously on fire
Behat Provides
Cucumber syntax
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
C
O
D
E
SN
IPPET
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 is mysteriously on fire
Cucumber Syntax
What’s missing?
Scenario:
Given that the wizard has 10 cookies
And the Bard eats 1 cookie
# The triggered fire spell fizzled due to OutOfManaException
Then the Bard is mysteriously on fire
1 scenario (1 failed)
3 steps (2 passed, 1 failed)
0m0.03s (14.19Mb)
Remember {P} C {Q}
Set your starting states
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 is mysteriously on fire
Remember {P} C {Q}
Set your starting states
???
As a leader, I want to ensure harmony and
mutual trust, so that we work as a team
User stories
As a <role>, I want to <desire> so that
<benefit>
Features are your contract with the stakeholders
User stories
Features
Backgrounds are your restrictions or global constraints
Background
Scenarios are the use cases that outline the user story
Scenarios
Front end testing is
“code coverage” for
your user stories
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
C
O
D
E
SN
IPPET
OK...
Dropping the party
https://github.com/opencfp/opencfp
$ composer require behat/behat="^3.0.5"
Getting started
$ composer require behat/mink-extension="^2.0"
Behat (cucumber syntax)
Mink (browser emulator)
Web drivers
$ composer require behat/mink-goutte-driver="^1.0"
$ composer require behat/mink-selenium2-driver="^1.2"
$ ./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
use BehatMinkExtensionContextMinkContext;
class FeatureContext extends MinkContext … {
…
}
Context
FeatureContext.php
C
O
D
E
SN
IPPET
C
O
D
E
SN
IPPET
$ ./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?
default:
suites:
default:
paths: [ %paths.base%/features/ ]
contexts: [ FeatureContext ]
extensions:
BehatMinkExtension:
base_url: "[your website]"
sessions:
default:
goutte: ~
Configuration
behat.yml
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
$ ./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
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
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
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
C
O
D
E
SN
IPPET
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
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….
Migration and seeding
Doctrine, Propel, Laravel, Phinx
Phinx to the rescue
SID
E
N
O
TE
$ composer require robmorgan/phinx="~0.5"
Phinx to the rescue
Install
$ php vendor/bin/phinx init
Phinx by Rob Morgan - https://phinx.org. version 0.5.1
Created ./phinx.xml
Configuration
$ php vendor/bin/phinx create InitialMigration
Creating
SID
E
N
O
TE
#!/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
A bit extreme?
run-behat-tests.sh
SAVEPOINT identifier;
# Run tests
ROLLBACK TO SAVEPOINT identifier;
RELEASE SAVEPOINT identifier;
Transaction/Rollback
Roll your own solution
Activation emails?
smtp-sink, FakeSMTP, etc
# 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
Or….
you could just read the activation code from
the database directly
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
default:
suites:
default:
paths: [ %paths.base%/features/ ]
contexts:
- FeatureContext
- DatabaseContext:
- mysql:host=localhost;dbname=opencfp
- root
- 123
Configuration
behat.yml
SID
E
N
O
TE
Or….
actually send the email and read it via SMTP
How far is too far?
What are your priorities?
Taking it too far
This one is actually a true story
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
...
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."
Tyranny of JavaScript
Deleting a talk
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"
Then I should not see "Behat Talk Changed"
The text "Behat Talk Changed" appears in the text of this page,
but it should not. (BehatMinkExceptionResponseTextException)
// 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
default:
# …
BehatMinkExtension:
base_url: "[your website]"
sessions:
default:
goutte: ~
javascript:
selenium2:
browser: "firefox"
wd_host: http://[machine-running-selenium]:4444/wd/hub
Configuration
Setting up for Selenium
$ java -jar selenium-server-standalone-2.*.jar
Selenium
@javascript
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
$ ./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
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 follow "Delete"
And I accept alerts
Then I should not see "Behat Talk"
Enable JavaScript
talks.feature
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
Then I should not see "Behat Talk"
Demo
Prepare for explosions!
Advanced Usage
with extra bells and whistles
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
C
O
D
E
SN
IPPET
class FeatureContext … {
/**
* @Given /^(?:I )wait for AJAX to finish$/
*/
public function iWaitForAjaxToFinish() {
$this->getSession()->wait(5000, "(0 === jQuery.active)");
}
}
AJAX
The waiting game
C
O
D
E
SN
IPPET
class FeatureContext … {
/**
* @Given /^(?:I )press the letter :l$/
*/
public function iPressTheLetter($l) {
$s = "jQuery.event.trigger({type:'keypress', which:'$l'});";
$this->getSession()->evaluateScript($s);
}
}
Raw javascript
There might be a valid use case...
C
O
D
E
SN
IPPET
Scenario: Edit a talk
Given I am on "/dashboard"
And I remember "tr[id^='talk']" content as "Title"
When I follow "Edit"
And I fill in "title" with "New Title"
And I press "Update my talk!"
Then I should see "New Title"
And I should not see "memory:Title"
Transformations
Sometimes we need to remember
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
C
O
D
E
SN
IPPET
/**
* @Given /^I remember "(.*)" content as "(.*)"$/
*/
public function rememberContentOf($selector, $key) {
$e = $this->getSession()->getPage()->find("css", $selector);
if (!is_object($e)) {
throw new LogicException("Element $selector not found");
}
$value = $e->getValue() ? $e->getValue() : $e->getText();
$this->memory[$key] = $value;
}
Transformations
MemoryContext.php
C
O
D
E
SN
IPPET
CSS Selectors
Drives are just like browser, no one ever
supports everything properly...
SID
E
N
O
TE
use BehatBehatHookScopeAfterFeatureScope; // @AfterFeature
AfterScenarioScope; // @AfterScenario
AfterStepScope; // @AfterStep
BeforeFeatureScope; // @BeforeFeature
BeforeScenarioScope; // @BeforeScenario
BeforeStepScope; // @BeforeStep
FeatureScope; // @Feature
ScenarioScope; // @Scenario
StepScope; // @Step
Hooks
Listen in close
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
C
O
D
E
SN
IPPET
class FeatureContext … {
/**
* @AfterStep
*/
public function afterStep(AfterStepScope $scope) {
$code = $event->getTestResult()->getResultCode();
if ($code == TestResult::FAILED) {
// Take a screenshot
}
}
}
Hooks
FeatureContext.php
C
O
D
E
SN
IPPET
Some days everything is made of glass
Common Gotchas
Expect breakages
And that’s a good thing
Speed vs Coverage
Find the right balance
Keep Selenium updated
Browsers change faster than fashion trends
Beware tutorials
Some substantial changes were made
between version 2.5.* and 3.0.*
Questions?
or ask me later via @thomas_shone
Thank you
Photos from Flickr by John Morey, TrojanRat, Gerry Machen, USFS Region
5, Peregrina Tyss and Thomas Hawk. Photo from Pixabay by
Schwarzenarzisse
Follow at @thomas_shone
Hidden Extras
Surprise!
Complex Users
Dealing with very complex user states
Feature: Show relevant promotions to non-paying activated customers
Scenario: Show promotion pricing to referred clients from the EU
Given I create a user with:
| email-activated |
| is-EU-member |
| is-referred-client |
| NOT has-made-purchase |
Then ...
Setup
What attributes does our user have?
interface AttributeInterface {
// Get the required attributes to have this attribute
public function getDependencies();
// Does this user have this attribute?
public function has(MinkContext $context);
// Allocate this attribute to the user
public function allocate(MinkContext $context);
// Attempt to remove this attribute from the user
public function remove(MinkContext $context);
}
Attributes
AttributeInterface.php
C
O
D
E
SN
IPPET
class IsEUMember extends AttributeInterface {
public function getDependencies() { return ["email-activated"]; }
public function has(MinkContext $context) {
$context->visit("/profile");
$field = $context->getSession()
->getPage()
->findField("country");
return in_array($field->getValue(), $this->eu_countries);
}
// ...
}
Attributes
IsEUMember.php
C
O
D
E
SN
IPPET
// ...
public function allocate(MinkContext $context) {
$context->visit("/profile");
$context->selectOption("country", "Netherlands");
$context->pressButton("Update");
}
public function remove(MinkContext $context) {
$context->visit("/profile");
$context->selectOption("country", "UK"); // Future-proofing
$context->pressButton("Update");
}
Attributes
IsEUMember.php
C
O
D
E
SN
IPPET
// This class must handle dependency conflicts by examining the
// dependencies of each with/without attribute
class Request {
protected $attributes = [];
public function with(AttributeInterface $feature);
public function without(AttributeInterface $feature);
// List of attributes (including dependents) required
public function getWith();
// List of attributes (including dependents) that must be removed
public function getWithout();
}
Request
Request.php
C
O
D
E
SN
IPPET
class User {
public function provision(MinkContext $context, $request) {
$this->createNewUser($context);
foreach ($request->getWith() as $attr)
$attr->allocate($context);
foreach ($request->getWithout() as $attr)
$attr->remove($context);
}
public function canSupport($request);
}
User
User.php
C
O
D
E
SN
IPPET
use BehatGherkinNodeTableNode;
class FeatureContext … {
/**
* @Given /^(?:I )create a user with:$/
*/
public function iCreateAUser($type, TableNode $table) {
$attributes = $table->getRowsHash();
$request = new Request();
// Build using $request->with(...) & $request->without(...);
$user = new User($context, $request);
}
}
TableNode
Handling a table
C
O
D
E
SN
IPPET
Permutations
The reason why testing is painful
default:
suites:
web:
paths: [ %paths.base%/features/web ]
contexts: [ BaseContext, WebContext ]
api:
paths: [ %paths.base%/features/api ]
contexts: [ BaseContext, ApiContext ]
Configuration
Multiple contexts
default:
suites:
admin:
paths: [ %paths.base%/features/web ]
contexts: [ BaseContext, AdminContext ]
filters:
role: admin
speaker:
paths: [ %paths.base%/features/web ]
contexts: [ BaseContext, SpeakerContext ]
filters:
tags: @speaker
Configuration
Grouping and filtering
$ ./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
$ ./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
$ java -jar selenium-server-standalone-2.*.jar -role hub
Selenium Grid
$ java -jar selenium-server-standalone-2.*.jar -role node -hub http://
[gridserver]:4444/grid/register
Start the grid
Add a node
default:
extensions:
BehatMinkExtension:
sessions:
javascript:
selenium2:
wd_host: "http://127.0.0.1:4444/wb/hub"
capabilities:
version: ""
Selenium Grid
Configuration

I put on my mink and wizard behat - Confoo Canada

  • 1.
  • 2.
    I put onmy mink and wizard behat Questing in the world of front end testing
  • 3.
  • 4.
  • 5.
  • 6.
    Meet The Party Don’tfeed the druid after midnight
  • 7.
    Task Each test hasa different approach
  • 8.
  • 9.
  • 10.
  • 11.
  • 13.
  • 14.
  • 15.
    The glue Behat (cucumber syntax) Mink (browseremulation) Goutte (web driver) Selenium (web driver) Zombie (web driver) Guzzle (curl) Selenium RC (java) Zombie.js (node.js)
  • 16.
    Feature: Party harmony Asa 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 is mysteriously on fire Behat Provides Cucumber syntax
  • 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 C O D E SN IPPET
  • 18.
    Feature: Party harmony Asa 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 is mysteriously on fire Cucumber Syntax What’s missing?
  • 19.
    Scenario: Given that thewizard has 10 cookies And the Bard eats 1 cookie # The triggered fire spell fizzled due to OutOfManaException Then the Bard is mysteriously on fire 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 Asa 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 is mysteriously on 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
  • 22.
    User stories As a<role>, I want to <desire> so that <benefit>
  • 23.
    Features are yourcontract with the stakeholders User stories Features Backgrounds are your restrictions or global constraints Background Scenarios are the use cases that outline the user story Scenarios
  • 24.
    Front end testingis “code coverage” for your user stories
  • 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 C O D E SN IPPET
  • 26.
  • 27.
  • 28.
    $ composer requirebehat/behat="^3.0.5" Getting started $ composer require behat/mink-extension="^2.0" Behat (cucumber syntax) Mink (browser emulator) Web drivers $ composer require behat/mink-goutte-driver="^1.0" $ composer require behat/mink-selenium2-driver="^1.2"
  • 29.
    $ ./vendor/bin/behat --init +dfeatures - 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.
    use BehatMinkExtensionContextMinkContext; class FeatureContextextends MinkContext … { … } Context FeatureContext.php C O D E SN IPPET C O D E SN IPPET
  • 31.
    $ ./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.
    default: suites: default: paths: [ %paths.base%/features/] contexts: [ FeatureContext ] extensions: BehatMinkExtension: base_url: "[your website]" sessions: default: goutte: ~ Configuration behat.yml
  • 33.
    Feature: Authentication andauthorisation 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
  • 34.
    $ ./vendor/bin/behat --configbehat.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
  • 35.
    Feature: Authentication andauthorisation 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
  • 36.
    Scenario: Attempt tologin 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
  • 37.
    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 C O D E SN IPPET
  • 38.
    Scenario: Attempt tologin 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
  • 39.
    Feature: Authentication andauthorisation 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.
    Migration and seeding Doctrine,Propel, Laravel, Phinx
  • 41.
    Phinx to therescue SID E N O TE
  • 42.
    $ composer requirerobmorgan/phinx="~0.5" Phinx to the rescue Install $ php vendor/bin/phinx init Phinx by Rob Morgan - https://phinx.org. version 0.5.1 Created ./phinx.xml Configuration $ php vendor/bin/phinx create InitialMigration Creating SID E N O TE
  • 43.
    #!/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 A bit extreme? run-behat-tests.sh
  • 44.
    SAVEPOINT identifier; # Runtests ROLLBACK TO SAVEPOINT identifier; RELEASE SAVEPOINT identifier; Transaction/Rollback Roll your own solution
  • 45.
  • 46.
    # Stop thecurrently 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
  • 47.
    Or…. you could justread the activation code from the database directly
  • 48.
    class DatabaseContext { publicfunction __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
  • 49.
    default: suites: default: paths: [ %paths.base%/features/] contexts: - FeatureContext - DatabaseContext: - mysql:host=localhost;dbname=opencfp - root - 123 Configuration behat.yml SID E N O TE
  • 50.
    Or…. actually send theemail and read it via SMTP
  • 51.
    How far istoo far? What are your priorities?
  • 52.
    Taking it toofar This one is actually a true story
  • 53.
    Our first talk Setthe 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 ...
  • 54.
    Our first talk Talksubmission 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.
  • 56.
    Well that won’twork 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" Then I should not see "Behat Talk Changed" The text "Behat Talk Changed" appears in the text of this page, but it should not. (BehatMinkExceptionResponseTextException)
  • 57.
    // Guzzle usingweb 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.
    default: # … BehatMinkExtension: base_url: "[yourwebsite]" sessions: default: goutte: ~ javascript: selenium2: browser: "firefox" wd_host: http://[machine-running-selenium]:4444/wd/hub Configuration Setting up for Selenium
  • 59.
    $ java -jarselenium-server-standalone-2.*.jar Selenium @javascript 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
  • 60.
    $ ./vendor/bin/behat --tagsspeaker,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
  • 61.
    Feature: Submitting andmanaging 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 follow "Delete" And I accept alerts Then I should not see "Behat Talk" Enable JavaScript talks.feature
  • 62.
    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 Then I should not see "Behat Talk"
  • 63.
  • 64.
    Advanced Usage with extrabells and whistles
  • 65.
    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 C O D E SN IPPET
  • 66.
    class FeatureContext …{ /** * @Given /^(?:I )wait for AJAX to finish$/ */ public function iWaitForAjaxToFinish() { $this->getSession()->wait(5000, "(0 === jQuery.active)"); } } AJAX The waiting game C O D E SN IPPET
  • 67.
    class FeatureContext …{ /** * @Given /^(?:I )press the letter :l$/ */ public function iPressTheLetter($l) { $s = "jQuery.event.trigger({type:'keypress', which:'$l'});"; $this->getSession()->evaluateScript($s); } } Raw javascript There might be a valid use case... C O D E SN IPPET
  • 68.
    Scenario: Edit atalk Given I am on "/dashboard" And I remember "tr[id^='talk']" content as "Title" When I follow "Edit" And I fill in "title" with "New Title" And I press "Update my talk!" Then I should see "New Title" And I should not see "memory:Title" Transformations Sometimes we need to remember
  • 69.
    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 C O D E SN IPPET
  • 70.
    /** * @Given /^Iremember "(.*)" content as "(.*)"$/ */ public function rememberContentOf($selector, $key) { $e = $this->getSession()->getPage()->find("css", $selector); if (!is_object($e)) { throw new LogicException("Element $selector not found"); } $value = $e->getValue() ? $e->getValue() : $e->getText(); $this->memory[$key] = $value; } Transformations MemoryContext.php C O D E SN IPPET
  • 71.
    CSS Selectors Drives arejust like browser, no one ever supports everything properly... SID E N O TE
  • 72.
    use BehatBehatHookScopeAfterFeatureScope; //@AfterFeature AfterScenarioScope; // @AfterScenario AfterStepScope; // @AfterStep BeforeFeatureScope; // @BeforeFeature BeforeScenarioScope; // @BeforeScenario BeforeStepScope; // @BeforeStep FeatureScope; // @Feature ScenarioScope; // @Scenario StepScope; // @Step Hooks Listen in close
  • 73.
    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 C O D E SN IPPET
  • 74.
    class FeatureContext …{ /** * @AfterStep */ public function afterStep(AfterStepScope $scope) { $code = $event->getTestResult()->getResultCode(); if ($code == TestResult::FAILED) { // Take a screenshot } } } Hooks FeatureContext.php C O D E SN IPPET
  • 75.
    Some days everythingis made of glass Common Gotchas
  • 76.
  • 77.
    Speed vs Coverage Findthe right balance
  • 78.
    Keep Selenium updated Browserschange faster than fashion trends
  • 79.
    Beware tutorials Some substantialchanges were made between version 2.5.* and 3.0.*
  • 80.
    Questions? or ask melater via @thomas_shone
  • 81.
    Thank you Photos fromFlickr by John Morey, TrojanRat, Gerry Machen, USFS Region 5, Peregrina Tyss and Thomas Hawk. Photo from Pixabay by Schwarzenarzisse Follow at @thomas_shone
  • 82.
  • 83.
    Complex Users Dealing withvery complex user states
  • 84.
    Feature: Show relevantpromotions to non-paying activated customers Scenario: Show promotion pricing to referred clients from the EU Given I create a user with: | email-activated | | is-EU-member | | is-referred-client | | NOT has-made-purchase | Then ... Setup What attributes does our user have?
  • 85.
    interface AttributeInterface { //Get the required attributes to have this attribute public function getDependencies(); // Does this user have this attribute? public function has(MinkContext $context); // Allocate this attribute to the user public function allocate(MinkContext $context); // Attempt to remove this attribute from the user public function remove(MinkContext $context); } Attributes AttributeInterface.php C O D E SN IPPET
  • 86.
    class IsEUMember extendsAttributeInterface { public function getDependencies() { return ["email-activated"]; } public function has(MinkContext $context) { $context->visit("/profile"); $field = $context->getSession() ->getPage() ->findField("country"); return in_array($field->getValue(), $this->eu_countries); } // ... } Attributes IsEUMember.php C O D E SN IPPET
  • 87.
    // ... public functionallocate(MinkContext $context) { $context->visit("/profile"); $context->selectOption("country", "Netherlands"); $context->pressButton("Update"); } public function remove(MinkContext $context) { $context->visit("/profile"); $context->selectOption("country", "UK"); // Future-proofing $context->pressButton("Update"); } Attributes IsEUMember.php C O D E SN IPPET
  • 88.
    // This classmust handle dependency conflicts by examining the // dependencies of each with/without attribute class Request { protected $attributes = []; public function with(AttributeInterface $feature); public function without(AttributeInterface $feature); // List of attributes (including dependents) required public function getWith(); // List of attributes (including dependents) that must be removed public function getWithout(); } Request Request.php C O D E SN IPPET
  • 89.
    class User { publicfunction provision(MinkContext $context, $request) { $this->createNewUser($context); foreach ($request->getWith() as $attr) $attr->allocate($context); foreach ($request->getWithout() as $attr) $attr->remove($context); } public function canSupport($request); } User User.php C O D E SN IPPET
  • 90.
    use BehatGherkinNodeTableNode; class FeatureContext… { /** * @Given /^(?:I )create a user with:$/ */ public function iCreateAUser($type, TableNode $table) { $attributes = $table->getRowsHash(); $request = new Request(); // Build using $request->with(...) & $request->without(...); $user = new User($context, $request); } } TableNode Handling a table C O D E SN IPPET
  • 91.
    Permutations The reason whytesting is painful
  • 92.
    default: suites: web: paths: [ %paths.base%/features/web] contexts: [ BaseContext, WebContext ] api: paths: [ %paths.base%/features/api ] contexts: [ BaseContext, ApiContext ] Configuration Multiple contexts
  • 93.
    default: suites: admin: paths: [ %paths.base%/features/web] contexts: [ BaseContext, AdminContext ] filters: role: admin speaker: paths: [ %paths.base%/features/web ] contexts: [ BaseContext, SpeakerContext ] filters: tags: @speaker Configuration Grouping and filtering
  • 94.
    $ ./vendor/bin/behat --suiteadmin 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
  • 95.
    $ ./vendor/bin/behat --suitespeaker 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
  • 96.
    $ java -jarselenium-server-standalone-2.*.jar -role hub Selenium Grid $ java -jar selenium-server-standalone-2.*.jar -role node -hub http:// [gridserver]:4444/grid/register Start the grid Add a node
  • 97.