97
Booking.com WE ARE HIRING Work @ Booking: http://grnh.se/seomt7

I put on my mink and wizard behat - Confoo Canada

  • Upload
    xsist10

  • View
    14.717

  • Download
    3

Embed Size (px)

Citation preview

Booking.comW

E ARE HIRING

Work @ Booking: http://grnh.se/seomt7

I put on my mink and

wizard behatQuesting in the world of

front end testing

Hoare Logic{P} C {Q}

Hodor Logic{P} C {Q}

Why?What's the benefit?

Meet The PartyDon’t feed the druid after midnight

TaskEach test has a different approach

BarbarianQuality Assurance

RangerUnit Test

ClericContinuous Integration

WizardFront 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 harmonyAs 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 ProvidesCucumber 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 intoFeatureContext.php

CODE SNIPPET

Feature: Party harmonyAs 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 SyntaxWhat’s missing?

Scenario:Given that the wizard has 10 cookiesAnd the Bard eats 1 cookie

# The triggered fire spell fizzled due to OutOfManaExceptionThen 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 harmonyAs 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 chargedAnd 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 storiesAs a <role>, I want to <desire> so that

<benefit>

Features are your contract with the stakeholders

User storiesFeatures

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

CODE SNIPPET

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

InitializeCreate a new test suite

use Behat\MinkExtension\Context\MinkContext;

class FeatureContext extends MinkContext … {…

}

ContextFeatureContext.php

CODE SNIPPET

CODE SNIPPET

$ ./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>(?:[^"]|\\")*)"$/

ContextWhat does Mink bring to the table?

default: suites: default: paths: [ %paths.base%/features/ ] contexts: [ FeatureContext ] extensions: Behat\MinkExtension: base_url: "[your website]" sessions: default: goutte: ~

Configurationbehat.yml

Feature: Authentication and authorisationAs 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 "[email protected]" And I fill in "password" with "invalid" And I press "Login" Then I should see "Invalid Email or Password"

Our first featureauth.feature

$ ./vendor/bin/behat --config behat.yml features/auth.feature

Scenario: Attempt to login with an invalid accountGiven I am on "/login"When I fill in "email" with "[email protected]"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)

Victoryoutput

Feature: Authentication and authorisationAs 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 "[email protected]" with password "invalid" Then I should see "Invalid Email or Password"

Simplifyauth.feature

Scenario: Attempt to login with an invalid accountGiven I login as "[email protected]" 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();

}

Simplifyoutput

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");

}}

SimplifyFeatureContext.php

CODE SNIPPET

Scenario: Attempt to login with an invalid accountGiven I login as "[email protected]" with password "invalid"Then I should see "Invalid Email or Password"

1 scenarios (1 passed)2 steps (2 passed)

Simplifyoutput

Feature: Authentication and authorisationAs 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 "[email protected]" 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 hurdleThis ones easy, you do…. oh….

Migration and seedingDoctrine, Propel, Laravel, Phinx

Phinx to the rescue

SIDE NOTE

$ composer require robmorgan/phinx="~0.5"

Phinx to the rescueInstall

$ php vendor/bin/phinx initPhinx by Rob Morgan - https://phinx.org. version 0.5.1Created ./phinx.xml

Configuration

$ php vendor/bin/phinx create InitialMigration

Creating

SIDE NOTE

#!/usr/bin/env bashDATABASE="opencfp"mysql -e "DROP DATABASE IF EXISTS $DATABASE" -uroot -p123mysql -e "CREATE DATABASE $DATABASE" -uroot -p123vendor/bin/phinx migratevendor/bin/behat

A bit extreme?run-behat-tests.sh

SAVEPOINT identifier;

# Run tests

ROLLBACK TO SAVEPOINT identifier;RELEASE SAVEPOINT identifier;

Transaction/RollbackRoll your own solution

Activation emails?smtp-sink, FakeSMTP, etc

# Stop the currently running servicesudo 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-sinkrun-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 contextDatabaseContext.php

SIDE NOTE

default: suites: default: paths: [ %paths.base%/features/ ] contexts:

- FeatureContext- DatabaseContext:

- mysql:host=localhost;dbname=opencfp- root- 123

Configurationbehat.yml

SIDE NOTE

Or….actually send the email and read it via SMTP

How far is too far?What are your priorities?

Taking it too farThis one is actually a true story

Our first talkSet the stage

Feature: Manage paper submissionsIn order to ensure that speakers can submit their papersAs an speaker I need to be able to manage my own submissions

Background:There is a speaker registered as "[email protected]" with a

password "secrets"I login as "[email protected]" with password "secrets"

Scenario: Add a new talk to our submissions...

Our first talkTalk submission in 3, 2, 1...

Scenario: Add a new talk to our submissionsGiven 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 JavaScriptDeleting a talk

Well that won’t worktalks.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 talkGiven 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. (Behat\Mink\Exception\ResponseTextException)

// Guzzle using web scraperbehat/mink-goutte-driver

// Java-based distributed browser workers (support JavaScript)behat/mink-selenium2-driverbehat/mink-sahi-driver

// node.js headless browser proxy (support JavaScript)behat/mink-zombie-driver

DriversSome take the scenic route

default: # … Behat\MinkExtension: base_url: "[your website]" sessions: default: goutte: ~ javascript: selenium2: browser: "firefox"

wd_host: http://[machine-running-selenium]:4444/wd/hub

ConfigurationSetting up for Selenium

$ java -jar selenium-server-standalone-2.*.jar

Selenium

@javascriptFeature: 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

TagsRun specific tags

@speakerFeature: 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 ...

SIDE NOTE

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 alertsThen I should not see "Behat Talk"

Enable JavaScripttalks.feature

Run as JavaScripttalks.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 talkGiven create a talk called "Behat Talk"And I am on "/dashboard"When I follow "Delete"And I accept alertsThen I should not see "Behat Talk"

DemoPrepare for explosions!

Advanced Usagewith extra bells and whistles

class FeatureContext … {public function takeAScreenshotCalled($filename) { $driver = get_class($this->getSession()->getDriver());

if ($driver == 'Behat\Mink\Driver\Selenium2Driver') {$ss = $this->getSession()

->getDriver()->getScreenshot();

file_put_contents($filename, $ss);}

}}

ScreenshotFeatureContext.php

CODE SNIPPET

class FeatureContext … {/** * @Given /^(?:I )wait for AJAX to finish$/ */public function iWaitForAjaxToFinish() {

$this->getSession()->wait(5000, "(0 === jQuery.active)");}

}

AJAXThe waiting game

CODE SNIPPET

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 javascriptThere might be a valid use case...

CODE SNIPPET

Scenario: Edit a talkGiven 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"

TransformationsSometimes 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];}

}

TransformationsMemoryContext.php

CODE SNIPPET

/** * @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;

}

TransformationsMemoryContext.php

CODE SNIPPET

CSS SelectorsDrives are just like browser, no one ever

supports everything properly...

SIDE NOTE

use Behat\Behat\Hook\Scope\AfterFeatureScope; // @AfterFeature \AfterScenarioScope; // @AfterScenario \AfterStepScope; // @AfterStep \BeforeFeatureScope; // @BeforeFeature \BeforeScenarioScope; // @BeforeScenario \BeforeStepScope; // @BeforeStep \FeatureScope; // @Feature \ScenarioScope; // @Scenario \StepScope; // @Step

HooksListen 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

}}

HooksFeatureContext.php

CODE SNIPPET

class FeatureContext … {/** * @AfterStep */public function afterStep(AfterStepScope $scope) {

$code = $event->getTestResult()->getResultCode();if ($code == TestResult::FAILED) {

// Take a screenshot}

}}

HooksFeatureContext.php

CODE SNIPPET

Some days everything is made of glass

Common Gotchas

Expect breakagesAnd that’s a good thing

Speed vs CoverageFind the right balance

Keep Selenium updatedBrowsers change faster than fashion trends

Beware tutorialsSome substantial changes were made

between version 2.5.* and 3.0.*

Questions?or ask me later via @thomas_shone

Thank youPhotos 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 ExtrasSurprise!

Complex UsersDealing with very complex user states

Feature: Show relevant promotions to non-paying activated customers

Scenario: Show promotion pricing to referred clients from the EUGiven I create a user with: | email-activated | | is-EU-member | | is-referred-client | | NOT has-made-purchase |Then ...

SetupWhat attributes does our user have?

interface AttributeInterface {// Get the required attributes to have this attributepublic function getDependencies();// Does this user have this attribute?public function has(MinkContext $context);// Allocate this attribute to the userpublic function allocate(MinkContext $context);// Attempt to remove this attribute from the userpublic function remove(MinkContext $context);

}

AttributesAttributeInterface.php

CODE SNIPPET

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);}// ...

}

AttributesIsEUMember.php

CODE SNIPPET

// ...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");

}

AttributesIsEUMember.php

CODE SNIPPET

// This class must handle dependency conflicts by examining the// dependencies of each with/without attributeclass Request {

protected $attributes = [];public function with(AttributeInterface $feature);public function without(AttributeInterface $feature);// List of attributes (including dependents) requiredpublic function getWith();// List of attributes (including dependents) that must be removedpublic function getWithout();

}

RequestRequest.php

CODE SNIPPET

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);}

UserUser.php

CODE SNIPPET

use Behat\Gherkin\Node\TableNode;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);

}}

TableNodeHandling a table

CODE SNIPPET

PermutationsThe 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 ]

ConfigurationMultiple 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

ConfigurationGrouping and filtering

$ ./vendor/bin/behat --suite admin

SuitesRun 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

SuitesRun a specific suite

@speakerFeature: 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: Behat\MinkExtension: sessions: javascript: selenium2: wd_host: "http://127.0.0.1:4444/wb/hub" capabilities: version: ""

Selenium GridConfiguration