Behat - Beyond the Basics (2016 - SunshinePHP)

84
Behat: Beyond the Basics @jessicamauerhan @SunshinePHP | 2-5-16 | https://joind.in/talk/9f341

Transcript of Behat - Beyond the Basics (2016 - SunshinePHP)

Page 1: Behat - Beyond the Basics (2016 - SunshinePHP)

Behat: Beyond the Basics

@jessicamauerhan@SunshinePHP | 2-5-16 | https://joind.in/talk/9f341

Page 2: Behat - Beyond the Basics (2016 - SunshinePHP)

Introduction

@[email protected]

Senior Test EngineerGrovo Learning, Inc.

Page 3: Behat - Beyond the Basics (2016 - SunshinePHP)

What is Behavior Driven

Development?

Behaviour-driven development is an “outside-in” methodology. It starts at the outside by identifying business outcomes, and then drills down into the feature set that will achieve those outcomes. Each feature is captured as a “story”, which defines the scope of the feature along with its acceptance criteria.

Dan North, “What’s in a Story”http://dannorth.net/whats-in-a-story

It’s the idea that you start by writing human-readable sentences that describe a feature of your application and how it should work, and only then implement this behavior in software.

Behat Documentationhttp://docs.behat.org/en/v2.5

Page 4: Behat - Beyond the Basics (2016 - SunshinePHP)

BDD is not about...

Well Designed CodeAutomated TestingImplementationUI Testing

Page 5: Behat - Beyond the Basics (2016 - SunshinePHP)

Why Practice Behavior Driven

Development?

“You can turn an idea for a requirement into implemented, tested, production-ready code simply and effectively, as long as the requirement is specific enough that everyone knows what’s going on.”

Dan North, “What’s in a Story”http://dannorth.net/whats-in-a-story

Page 6: Behat - Beyond the Basics (2016 - SunshinePHP)

GherkinBusiness Readable (Writable?), Domain Specific Language

Living Documentation / User Manual

Story: Feature Fileone Feature with a narrative

one or more Scenarios

Acceptance Criteria: Scenarios

Page 7: Behat - Beyond the Basics (2016 - SunshinePHP)

Narrative

As a [role]I want [feature]So that [benefit / business reason]

Use “5 Whys” to determine narrative

Feature: Account Holder Withdraws CashFeature: Short, Descriptive, Action

Acceptance Criteria: Scenarios

Given: Exact Context

When: Action/Event

Then: Outcomes

And/But: More of the same...

Scenario: Account has sufficient fundsGiven the account balance is $100And the card is validAnd the machine contains enough moneyWhen I request $20Then the ATM should dispense $20And the account balance should be $80And the card should be returned

Scenario: Account has insufficient fundsGiven the account balance is $10And the card is validAnd the machine contains at least the amount of my balanceWhen I request $20Then the ATM should not dispense any moneyAnd the ATM should say there are insufficient fundsAnd my account balance should be $10And the card should be returned

Scenario: Card has been disabledGiven the card is disabledWhen I request $20Then the ATM should retain the cardAnd the ATM should say the card has been retained

As an Account Holder I want to withdraw cash from an ATM So that I can get money when the bank is closed

Page 8: Behat - Beyond the Basics (2016 - SunshinePHP)

Gherkin Keywords: Auto-Generated StepsYou can implement step definitions for undefined steps with these snippets: /** * @When /^I select Texas from the states list$/ */ public function iSelectTexasFromTheStatesList() { throw new PendingException(); } /** * @Then /^I should see the list of services offered$/ */ public function iShouldSeeTheListOfServicesOffered() { throw new PendingException(); }

Page 9: Behat - Beyond the Basics (2016 - SunshinePHP)

BDD is about...

Communication CollaborationDocumentationPreventing User-Facing

Regressions

Page 10: Behat - Beyond the Basics (2016 - SunshinePHP)

Writing Features

Page 11: Behat - Beyond the Basics (2016 - SunshinePHP)

Feature: View Countdown before Broadcast As a user viewing a broadcast page before the broadcast starts I want to see a countdown timer So that I can know how long until the broadcast actually starts

Scenario: View Countdown before Broadcast

Given I view the “Future Broadcast” broadcast Then I should see "Future Broadcast" on the pageAnd I should see "Future Broadcast Author" on the pageAnd I should see "This broadcast begins at 6:00 pm EST" on the pageAnd I should see a countdown timer

ProcessAuthor describes Feature

with implementation specific example

Developer adds fixture data

IssuesTest only works before 6pmWhat is the intent?Confusion for business users

Page 12: Behat - Beyond the Basics (2016 - SunshinePHP)

Feature: View Countdown before Broadcast As a user viewing a broadcast page before the broadcast starts I want to see a countdown timer So that I can know how long until the broadcast actually starts

Scenario: View Countdown before Broadcast

Given there is a broadcast scheduled for the futureWhen I view that broadcast’s pageThen I should see the broadcast titleAnd I should see the broadcast author’s nameAnd I should see "This broadcast begins at" followed by the start time in ESTAnd I should see a countdown timer

ChangesGiven now explains exact

contextTest is no longer time-

dependentIntent - not implementationWhy?Overall understandingCommunication valueHelp identify poorly written

code

Page 13: Behat - Beyond the Basics (2016 - SunshinePHP)

Feature: Customer Views Product and Services Catalog As a customer I want to view the product catalog So that I can browse products and services

Scenario: Customer views Product CatalogGiven I view the catalogWhen I select "Texas" from the list of statesThen I should see the list of products for sale

Scenario: Customer views Local Services in Product CatalogGiven I view the catalogWhen I select "Texas" from the list of statesThen I should see the list of services offered

IssuesWhat is the intent?Is Texas relevant?

Page 14: Behat - Beyond the Basics (2016 - SunshinePHP)

Feature: Customer Views Product and Services Catalog As a customer I want to view the product catalog So that I can browse products and services

Scenario: Customer views Product CatalogGiven I view the catalogWhen I select "Texas" from the list of statesThen I should see the list of products for sale

Scenario: Customer views Local Services in Product CatalogGiven I view the catalogWhen I select "Texas" from the list of statesThen I should see the list of services offered

IssuesWhat is the intent?Is Texas relevant?

if not: implementation detail

Scenario: Customer views Product CatalogGiven I view the catalogWhen I select a state from the list of statesThen I should see the list of products for sale

Scenario: Display Local Services in Product CatalogGiven I view the catalogWhen I select a state from the list of statesThen I should see the list of services offered

Page 15: Behat - Beyond the Basics (2016 - SunshinePHP)

Feature: Customer Views Product and Services Catalog As a customer I want to view the product catalog So that I can browse products and services

Scenario: Customer views Product CatalogGiven I view the catalogWhen I select "Texas" from the list of statesThen I should see the list of products for sale

Scenario: Customer views Local Services in Product CatalogGiven I view the catalogWhen I select "Texas" from the list of statesThen I should see the list of services offered

IssuesWhat is the intent?Is Texas relevant?

if not: implementation detailif yes: uncover business value

and context

Scenario: Customer views Product CatalogGiven I view the catalogWhen I select a state from the list of statesThen I should see the list of products for sale

Scenario: Display Local Services in Product CatalogGiven the company has a regional office in a stateWhen I view the catalogAnd I select that state from the list of statesThen I should see the list of services offered

Page 16: Behat - Beyond the Basics (2016 - SunshinePHP)

Feature: Customer Views Product and Services Catalog As a customer I want to view the product catalog So that I can browse products and services

Scenario: Customer views Product CatalogGiven I view the catalogWhen I select "Texas" from the list of statesThen I should see the list of products for sale

Scenario: Customer views Local Services in Product CatalogGiven I view the catalogWhen I select "Texas" from the list of statesThen I should see the list of services offered

NarrativeUse narrative to explain

business value for featureNot processed in Behat 2.5Behat 3.0+ role can be used

to specify which context files are used

Feature: Customer Views Texas-Specific Catalog As a customer in Texas I want to view the products and services for sale in Texas So that I can buy them

Scenario: Display Local Services when viewing Texas

Given I view the catalogAnd I select Texas from the list of statesThen I should see the products for sale in TexasAnd I should see the list of services offered in Texas

Page 17: Behat - Beyond the Basics (2016 - SunshinePHP)

ImplementationWhen I select "Texas" from the states list

/*** @When /^I select "([^"]*)" from the states list$/ */public function iSelectFromTheStatesList($arg1){}

IntentWhen I select Texas from the states list

/*** @When /^I select Texas from the states list$/*/public function iSelectTexasFromTheStatesList(){}

Demonstrate Intent by Removing Variables

Shows specific intent rather than an example of behavior

Indicates there is an important business value to the state

Impossible for anyone to plug in another state

Page 18: Behat - Beyond the Basics (2016 - SunshinePHP)

Feature: Customer Views Product and Services Catalog As a customer I want to view the product catalog So that I can browse products and services

Scenario: Customer views Product CatalogGiven I view the catalogWhen I select a state from the list of statesThen I should see the list of products for sale

Scenario: Display Local Services in Product CatalogGiven the company has a regional office in a stateWhen I view the catalogAnd I select that state from the list of statesThen I should see the list of services offered

echo '<h1>Products</h1>';foreach ($products AS $product) { echo '<p>' . $product->getName() . '</p>';}

echo '<h1>Services</h1>';foreach ($services AS $service) { echo '<p>' . $service->getName() . '</p>';}

Scenario: Don’t Display Local Services in Product Catalog For States With No Regional Office

Given the company does not have a regional office in a stateWhen I view the catalogAnd I select that state from the list of statesThen I should not see a list of services offered

Page 19: Behat - Beyond the Basics (2016 - SunshinePHP)

Feature: Customer Views Product and Services Catalog As a customer I want to view the product catalog So that I can browse products and services

Scenario: Customer views Product CatalogGiven I view the catalogWhen I select a state from the list of statesThen I should see the list of products for sale

Scenario: Display Local Services in Product CatalogGiven the company has a regional office in a stateWhen I view the catalogAnd I select that state from the list of statesThen I should see the list of services offered

Scenario: Don’t Display Local Services in Product Catalog For States With No Regional Office Given the company does not have a regional office in a stateWhen I view the catalogAnd I select that state from the list of statesThen I should not see a list of services offered

echo '<h1>Products</h1>';foreach ($products AS $product) { echo '<p>' . $product->getName() . '</p>';}

if(count($services) > 0) { echo '<h1>Services</h1>'; foreach ($services AS $service) { echo '<p>' . $service->getName() . '</p>'; }}

Page 20: Behat - Beyond the Basics (2016 - SunshinePHP)

Writing Great FeaturesExact Context

Independent Scenarios & Features

Intention, not Implementation

Defined Narrative

All Paths Explored

Feature “Smells”Time Dependency

Interdependency

Multi-Scenario Scenarios

Missing Scenarios

Overuse of Variables

Examples of Behavior (Implementation, not Intention)

Page 21: Behat - Beyond the Basics (2016 - SunshinePHP)

Behat

Page 22: Behat - Beyond the Basics (2016 - SunshinePHP)

Drivershttp://mink.behat.org/en/latest

Browser EmulatorFaster

HTTP Info: Response Header, Status Code

Real Browser Driverexecute JavaScript

Tell if element is actually visible on the page

interact with browser

Page 23: Behat - Beyond the Basics (2016 - SunshinePHP)

Hooks

Page 24: Behat - Beyond the Basics (2016 - SunshinePHP)

Capturing Screenshot on Error/** @AfterScenario */public function afterScenario($event){ if ($event->getResult() == Event\StepEvent::FAILED) { $imageData = $this->getSession()->getDriver()->getScreenshot(); $imagePath = $this->getArtifactsDir() . time() . '.png'; file_put_contents($imagePath, $imageData); }}

Page 25: Behat - Beyond the Basics (2016 - SunshinePHP)

Hooks & Tags/** @AfterScenario @javascript */public function afterScenario($event){ if ($event->getResult() == Event\StepEvent::FAILED) { $imageData = $this->getSession()->getDriver()->getScreenshot(); $imagePath = $this->getArtifactsDir() . time() . '.png'; file_put_contents($imagePath, $imageData); }}

Page 26: Behat - Beyond the Basics (2016 - SunshinePHP)

Hooks & Tags (Multiple Tags)/** @AfterScenario @javascript,@screenshot */public function afterScenario($event){ if ($event->getResult() == Event\StepEvent::FAILED) { $imageData = $this->getSession()->getDriver()->getScreenshot(); $imagePath = $this->getArtifactsDir() . time() . '.png'; file_put_contents($imagePath, $imageData); }}

Page 27: Behat - Beyond the Basics (2016 - SunshinePHP)

Hooks: Dealing with AJAX (jQuery & Angular)/** @BeforeStep @javascript */public function beforeStep($event){ $waitTime = 5000; $jqDefined = "return (typeof jQuery != 'undefined')"; $active = '(0 === jQuery.active && 0 === jQuery(\':animated\').length)'; if ($this->getSession()->evaluateScript($jqDefined)) { $this->getSession()->wait($waitTime, $active); }}

//Angular: https://gist.github.com/jmauerhan/f839926ea527ff5e74e7

Page 28: Behat - Beyond the Basics (2016 - SunshinePHP)

Fixture Datanamespace Acme\AppBundle\DataFixtures;

use Doctrine\Common\Persistence\ObjectManager;use Doctrine\Common\DataFixtures\FixtureInterface;

class UserFixtureLoader implements FixtureInterface{ public function load(ObjectManager $manager) { $user = new User(); $user->setUsername('admin'); $user->setPassword('password');

$manager->persist($user); $manager->flush(); }}

Page 29: Behat - Beyond the Basics (2016 - SunshinePHP)

Load Fixture Data/** @BeforeFeature */public function beforeFeatureReloadDatabase($event){ $loader = new Loader(); $directory = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'DataFixtures';

$loader->loadFromDirectory($directory);

$entityManager = $this->getEntityManager(); $purger = new ORMPurger(); $executor = new ORMExecutor($entityManager, $purger); $executor->execute($loader->getFixtures());}

Page 30: Behat - Beyond the Basics (2016 - SunshinePHP)

Steps

Page 31: Behat - Beyond the Basics (2016 - SunshinePHP)

Multiple Regular Expressions/*** @Given /^I view the catalog$/* @Given /^I am viewing the catalog$/*/public function iViewTheCatalog(){ $this->getPage('Catalog')->open();}

Page 32: Behat - Beyond the Basics (2016 - SunshinePHP)

Case Insensitive - Flag Given I view the catalog Given I view the Catalog

/*** @Given /^I am viewing the catalog$/i*/public function iViewTheCatalog(){ $this->getPage('Catalog')->open();}

Page 33: Behat - Beyond the Basics (2016 - SunshinePHP)

Given I view the catalog Given I view the Catalog

/*** @Given /^I am viewing the (?i)catalog$/*/public function iViewTheCatalog(){ $this->getPage('Catalog')->open();}

Case Insensitive - Inline

Page 34: Behat - Beyond the Basics (2016 - SunshinePHP)

Quoted VariablesThen I should see an "error" message

/*** @Given /^I should see an "([^"])" message$/*/public function iShouldSeeAnMessage($arg1){

}

Page 35: Behat - Beyond the Basics (2016 - SunshinePHP)

Then I should see an error message

/*** @Given /^I should see an (.*) message$/*/public function iShouldSeeAnMessage($arg1){

}

Unquoted Variables

Page 36: Behat - Beyond the Basics (2016 - SunshinePHP)

Unquoted Variables with List of OptionsThen I should see an error messageThen I should see a success messageThen I should see a warning message

/*** @Given /^I should see an? (error|success|warning) message$/*/public function iShouldSeeAnMessage($messageType){ $class = '.alert-'.$messageType; $this->assertElementExists($class, 'css');}

Page 37: Behat - Beyond the Basics (2016 - SunshinePHP)

Optional VariablesThen I should see an error messageThen I should see an error message that says "Stop!"

/*** @Given /^I should see an? (error|success|warning) message$/* @Given /^I should see an? (error|success|warning) message that says "([^"]*)"$/*/public function iShouldSeeAnMessageThatSays($messageType, $message = null){ $class = '.alert -' . $messageType; $this->assertElementExists($class, 'css'); if ($message !== null) { $this->assertElementContainsText($class, 'css', $message); }}

Page 38: Behat - Beyond the Basics (2016 - SunshinePHP)

Non-Capturing GroupsThen I view the catalog for "Texas"Then I am viewing the catalog for "Texas"

/*** @Given /^I view the catalog for "([^"]*)"$/* @Given /^I am viewing the catalog "([^"]*)"$/*/public function iViewTheCatalogForState($stateName){ $args = ['stateName' => $stateName]; $this->getPage('Catalog')->open($args);}

Page 39: Behat - Beyond the Basics (2016 - SunshinePHP)

Non-Capturing GroupsThen I view the catalog for "Texas"Then I am viewing the catalog for "Texas"

/*** @Given /^I (?:am viewing|view) the catalog for "([^"]*)"$/*/public function iViewTheCatalogForState($stateName){ $args = ['stateName' => $stateName]; $this->getPage('Catalog')->open($args);}

Page 40: Behat - Beyond the Basics (2016 - SunshinePHP)

Step Definition Changes in Behat 3.xThen I view the catalog for "Texas"Then I view the catalog for Texas

/*** @Given I view the catalog for :stateName*/public function iViewTheCatalogForState($stateName){ $args = ['stateName' => $stateName]; $this->getPage('Catalog')->open($args);}

Page 41: Behat - Beyond the Basics (2016 - SunshinePHP)

Contexts

Page 42: Behat - Beyond the Basics (2016 - SunshinePHP)

SubContext (Behat 2.x)namespace Acme\AppBundle\Context;

use Behat\MinkExtension\Context\MinkContext;

class FeatureContext extends MinkContext{ public function __construct(array $parameters) { $this->parameters = $parameters; $this->useContext('MessageContext', new MessageContext()); }}

Page 43: Behat - Beyond the Basics (2016 - SunshinePHP)

Several SubContexts (Behat 2.x)[...]

public function __construct(array $parameters){ $this->parameters = $parameters; $this->useContext('AdminContext', new AdminContext()); $this->useContext('FormContext', new FormContext()); $this->useContext('EditUserContext', new EditUserContext()); $this->useContext('ApiContext', new ApiContext());}

Page 44: Behat - Beyond the Basics (2016 - SunshinePHP)

Alias All SubContexts Automatically (Behat 2.x)private function loadSubContexts(){ $finder = new Finder(); $finder->name('*Context.php') ->notName('FeatureContext.php') ->notName('CoreContext.php'); $finder->files()->in(__DIR__);

}

Page 45: Behat - Beyond the Basics (2016 - SunshinePHP)

Alias All SubContexts Automatically (Behat 2.x)private function loadSubContexts(){ $finder = new Finder(); $finder->name('*Context.php') ->notName('FeatureContext.php') ->notName('CoreContext.php'); $finder->files()->in(__DIR__);

foreach ($finder as $file) { $className = $file->getBaseName('.php');

$namespace = __NAMESPACE__ . '\\' . $file->getRelativePath(); if (substr($namespace, -1) !== '\\') { $namespace .= '\\'; } $reflectionClass = new ReflectionClass($namespace . $className); $this->useContext($className, $reflectionClass->newInstance()); }}

Page 46: Behat - Beyond the Basics (2016 - SunshinePHP)

<?php

namespace Acme\AppBundle\Context;

class FeatureContext extends CoreContext{ /** @Given /^I should see an? (error|success|warning) message that says "([^"])"$/ */ public function iShouldSeeAnMessageThatSays($messageType, $message = null) { $class = '.alert -' . $messageType; $element = $this->getPage()->find('css', $class);

$actualMessage = $element->getText(); $this->assertEqual($actualMessage, $message); }}

Message Context

Page 47: Behat - Beyond the Basics (2016 - SunshinePHP)

Find Required Element Shortcutpublic function findRequiredElement($locator, $selector = 'xpath', $parent = null){ if (null === $parent) { $parent = $this->getPage(); }

$element = $parent->find($selector, $locator); if (null === $element) { throw new ElementNotFoundException($this->getSession(), null, $selector, $locator); }

return $element;}

Page 48: Behat - Beyond the Basics (2016 - SunshinePHP)

Message Contextnamespace Acme\AppBundle\Context;

class FeatureContext extends CoreContext{ /** @Given /^I should see an? (\w*) message that says "([^"])"$/ */ public function iShouldSeeAnMessageThatSays($messageType, $message = null) { $class = '.alert -' . $messageType; $element = $this->findRequiredElement($class, 'css');

$actualMessage = $element->getText(); $this->assertEqual($actualMessage, $message); }}

Page 49: Behat - Beyond the Basics (2016 - SunshinePHP)

CoreContext with Step Annotation causes Error [Behat\Behat\Exception\RedundantException] Step "/^I should be redirected to "([^"]*)"$/" is already defined in Acme\AppBundle\Context\FeatureContext::iShouldBeRedirectedTo() Acme\AppBundle\Context\FeatureContext::iShouldBeRedirectedTo() Acme\AppBundle\Context\MessageContext::iShouldBeRedirectedTo()

Page 50: Behat - Beyond the Basics (2016 - SunshinePHP)

Reusing Multiple Steps

Page 51: Behat - Beyond the Basics (2016 - SunshinePHP)

Reusing Multiple StepsScenario: Upload a csv file Given I am viewing the csv import form When I attach a csv to "Import File" And I submit the form Then I should see a success message And I should see the file review screen

Scenario: Review and Confirm the csv file Given I have uploaded a csv And I am viewing the file review screen When I select a property for each column And I submit the form Then I should see a success message

Page 52: Behat - Beyond the Basics (2016 - SunshinePHP)

Meta-Stepsuse Behat\Behat\Context\Step;

class FeatureContext{ /** @Given /^I have uploaded a csv$/ */ public function iHaveUploadedACsv() { return [ new Step\Given('I am viewing the csv import form'), new Step\When('I attach a csv to "Import File"'), new Step\When('I submit the form') ]; }}

Page 53: Behat - Beyond the Basics (2016 - SunshinePHP)

Meta-Steps With Multi-line Argumentsuse Behat\Behat\Context\Step;use Behat\Gherkin\Node\PyStringNode;

class FeatureContext{ /** @Given /^I should see the file review screen$/ */ public function iShouldSeeTheFileReviewScreen() { $content = 'Please review your file .' . PHP_EOL . 'Press Submit to continue'; $pyString = new PyStringNode($content); return new Step\Given('I should see', $pyString); }}

Page 54: Behat - Beyond the Basics (2016 - SunshinePHP)

Direct Method Call - Same Context/** @Given /^I should see an error about the file type$/ */public function iShouldSeeAnErrorAboutTheFileType(){ $message = 'This file type is invalid'; $this->iShouldSeeAnMessageThatSays('error', $message);}

/** @Given /^I should see an? (.*) message that says "([^"])"$/ */public function iShouldSeeAnMessageThatSays($messageType, $message = null){ $class = '.alert -' . $messageType; $this->assertElementExists($class, 'css'); if ($message !== null) { $this->assertElementContainsText($class, 'css', $message); }}

Page 55: Behat - Beyond the Basics (2016 - SunshinePHP)

Direct Method Call to Another Context (Behat 2.x)/*** @Given /^I should see an error about the file type$/*/public function iShouldSeeAnErrorAboutTheFileType(){ $message = "This file type is invalid"; $this->getMainContext() ->getSubContext('MessageContext') ->iShouldSeeAnMessageThatSays('error', $message);}

Page 56: Behat - Beyond the Basics (2016 - SunshinePHP)

Direct Method Call to Another Context (Behat 2.x)/*** @return MessageContext*/public function getMessageContext(){ return $this->getMainContext()->getSubContext('messageContext');}

/*** @Given /^I should see an error about the file type$/*/public function iShouldSeeAnErrorAboutTheFileType(){ $message = "This file type is invalid"; $this->getMessageContext()->iShouldSeeAnMessageThatSays('error', $message);}

Page 57: Behat - Beyond the Basics (2016 - SunshinePHP)

Suite Contexts (Behat 3.x)default: suites: default: paths: [ %paths.base%/features/core ] contexts: [FeatureContext, MessageContext]

Page 58: Behat - Beyond the Basics (2016 - SunshinePHP)

Store Other Contexts (Behat 3.x)use Behat\Behat\Context\Context;use Behat\Behat\Hook\Scope\BeforeScenarioScope;

class FeatureContext implements Context{ /** @var MessageContext */ private $messageContext;

/** @BeforeScenario */ public function gatherContexts(BeforeScenarioScope $scope) { $environment = $scope->getEnvironment(); $this->messageContext = $environment->getContext('MessageContext'); }}// http://docs.behat.org/en/v3.0/cookbooks/context_communication.html

Page 59: Behat - Beyond the Basics (2016 - SunshinePHP)

Direct Method Call to Another Context (Behat 3.x)use Behat\Behat\Context\Context;use Behat\Behat\Hook\Scope\BeforeScenarioScope;

class FeatureContext implements Context{ /** @var \Behat\MinkExtension\Context\MinkContext */ private $minkContext;

/** @BeforeScenario */ public function gatherContexts(BeforeScenarioScope $scope) { $environment = $scope->getEnvironment();

$this->minkContext = $environment->getContext('Behat\MinkExtension\Context\MinkContext'); }}// http://docs.behat.org/en/v3.0/cookbooks/context_communication.html

/*** @Given /^I should see an error about the file type$/*/public function iShouldSeeAnErrorAboutTheFileType(){ $message = "This file type is invalid"; $this->messageContext->iShouldSeeAnMessageThatSays('error', $message);}

Page 60: Behat - Beyond the Basics (2016 - SunshinePHP)

Direct Call● Like any other

method call● Hooks do not fire

(typically faster)● Moving Step

definitions might require refactor

Meta-StepsReturn a Step or array

of StepsHooks will fire

(could be slow)Moving Step

definitions does not break

Removed in 3.0

Page 61: Behat - Beyond the Basics (2016 - SunshinePHP)

Page Objects

Page 62: Behat - Beyond the Basics (2016 - SunshinePHP)

Page Objects Extension#terminalphp composer require "sensiolabs/behat-page-object-extension"

#config.ymldefault: extensions: SensioLabs\Behat\PageObjectExtension\Extension: ~

Page 63: Behat - Beyond the Basics (2016 - SunshinePHP)

Seminar Page ObjectScenario: Visit Seminar Page before Broadcast Time Given there is a seminar scheduled for the future When I visit that seminar's page Then I should see the seminar's name And I should see the seminar's author’s name And I should see "This seminar begins at" And I should see the seminar’s start time in EST And I should see a countdown timer

Scenario: Visit Seminar Page before Broadcast Time Given there is a seminar scheduled When I visit that seminar's page during the broadcast time Then I should see the seminar's name And I should see the seminar video And I should not see a countdown timer

Page 64: Behat - Beyond the Basics (2016 - SunshinePHP)

Seminar Page Objectnamespace Acme\AppBundle\PageObjects;

use SensioLabs\Behat\PageObjectExtension\PageObject\Page;

class Seminar extends Page{ protected $path = '/seminar/{id}';

}

Page 65: Behat - Beyond the Basics (2016 - SunshinePHP)

Seminar Page Objectnamespace Acme\AppBundle\PageObjects;

use SensioLabs\Behat\PageObjectExtension\PageObject\Page;

class Seminar extends Page{ protected $path = '/seminar/{id}';

protected $elements = [ 'Author Info' => ['css' => "#author"], 'Video' => ['xpath' => "//div[contains(@class, 'video')]"], 'Countdown Timer' => ['css' => ".timer"], ];}

Page 66: Behat - Beyond the Basics (2016 - SunshinePHP)

Element namespace Acme\AppBundle\PageObjects\Elements;

use SensioLabs\Behat\PageObjectExtension\PageObject\Element;

class AuthorInformation extends Element{ protected $selector = ['css' => "#author"];

public function getAuthorName() { return $this->find('css', '.name'); }

public function getAuthorPhoto() { return $this->find('xpath', '//img'); }

Page 67: Behat - Beyond the Basics (2016 - SunshinePHP)

Interesting Interactions

Page 68: Behat - Beyond the Basics (2016 - SunshinePHP)

CSV Report@javascriptScenario: View Summary Report Given a user in a group has registered for a seminar with a company And I am logged in as an admin And I am viewing the reports area When I download the "Summary" report Then I should see the following columns: | column | | Group | | Company | | Total | And I should see that user in the report

Page 69: Behat - Beyond the Basics (2016 - SunshinePHP)

File Download Test/** * @When /^I download the "([^"]*)" report$/ */public function iDownloadTheReport($reportName){ $xpath = "//a[normalize-space()='{$reportName}']"; $link = $this->findRequiredElement($xpath); $this->getSession()->visit('view-source:' . $link->getAttribute('href')); $content = $this->getSession()->getPage()->getContent(); $lines = explode(PHP_EOL, $content);

$this->csvRows = []; foreach ($lines as $line) { if (strlen(trim($line))) { $this->csvRows[] = str_getcsv($line); } }}

Page 70: Behat - Beyond the Basics (2016 - SunshinePHP)

Zip File Download@javascriptScenario: View Large Report Given I am viewing the reports area When I click "Export" for the "Extremely Large Report" report Then a zip file should be downloaded When I unzip the file and I open the extracted csv file Then I should see the following columns: | column | | Group | | Company | | Total |

Page 71: Behat - Beyond the Basics (2016 - SunshinePHP)

File Download Test/*** @When /^I click "Export" for the "([^"]*)" report$/*/public function iExportTheReport($reportName){ $this->file = $this->getArtifactsDir() . 'download-' . time() . '.zip'; file_put_contents($this->file, $this->getSession()->getDriver() > getContent());}/*** @Then /^a zip file should be downloaded$/*/public function aZipFileShouldBeDownloaded(){ $header = $this->getSession()->getDriver()->getResponseHeaders(); $this->assertContains($header['Content-Type'][0], 'application/forced-download'); $this->assertContains($header['Content-Disposition'][0], "zip");}

Page 72: Behat - Beyond the Basics (2016 - SunshinePHP)

File Download Test/*** @When /^I unzip the file and I open the extracted csv file$/*/public function iUnzipTheFileAndOpenCsvFile(){ $zip = new ZipArchive; $unzipped = $zip->open($this->file); $csv = $zip->getNameIndex(1); $zip->extractTo($this->getArtifactsDir()); $zip->close(); $fileRef = fopen($this->getArtifactsDir() . $csv, 'r');

$this->csvContents = []; while (($data = fgetcsv($fileRef)) !== false) { $this->csvContents[] = $data; } fclose($fileRef);}

Page 73: Behat - Beyond the Basics (2016 - SunshinePHP)

Confirm Text In PDF

/*** @Given /^I should see a PDF with the order total$/*/public function iShouldSeeAPdfWithTheOrderTotal(){ $total = 'Order Total: ' . $this->orderTotal; $this->getMainContext()->assertPageContainsText($total);}

@javascriptScenario: View PDF Receipt Given I am viewing my order history When I click "View Receipt" for an order Then I should see a PDF with the order total

Page 74: Behat - Beyond the Basics (2016 - SunshinePHP)

Behat Without The Browser

Page 75: Behat - Beyond the Basics (2016 - SunshinePHP)

Testing a Command Line Process with BehatScenario: Test User Import With a Large Data Set Given the system already has 100000 users And there is a company with 10000 of the users assigned to it And an admin has uploaded a spreadsheet for the company with 10000 rows When the system has begun to process the spreadsheet And I have waited 1 minute Then the batch process status should be set to "Running" or "Completed" And I should see at least 100 new users in the company

Page 76: Behat - Beyond the Basics (2016 - SunshinePHP)

The System Already Has 100000 Users /** @Given /^the system already has (\d+) users$/ */public function theSystemAlreadyHasUsers($numUsers){ $faker = $this->getFaker(); $userValues = [];

for ($i = 0; $i < $numUsers; $i++) { $firstname = addslashes($faker->firstName); $lastname = addslashes($faker->lastName); $username = $faker->username . $i; //unique $userValues[] = "('{$firstname}', '{$lastname}', '{$username}')"; } $userQuery = "INSERT INTO `user`(firstname, lastname, username) VALUES" . implode(', ', $userValues); $this->getEntityManager()->getConnection()->exec($userQuery);}

Page 77: Behat - Beyond the Basics (2016 - SunshinePHP)

There is a company with 10000 users assigned to it/** @Given /^there is a company with (\d+) of the users assigned to it$/ */public function thereIsACompanyWithOfTheUsersAssignedToIt($num){ $company = $this->generateCompany(); $conn = $this->getEntityManager()->getConnection();

$userCompanySQL = "INSERT INTO `user_company`(user_id, company_id)SELECT `user`.id, {$company->getId()} FROM `user` LIMIT {$num}"; $conn->exec($userCompanySQL);

$this->getEntityManager()->refresh($company); $companyUsersCount = $company->getUserCompanies()->count(); $this->assertGreaterThanOrEqual($num, $companyUsersCount);

$this->company = $company; $this->companyNumUsers = $companyUsersCount;}

Page 78: Behat - Beyond the Basics (2016 - SunshinePHP)

An Admin Has Uploaded a Spreadsheet/** @Given /^an admin has uploaded a spreadsheet for the company with (\d*) rows$/ */public function adminHasUploadedSpreadsheetForTheCompanyWithRows($numRows){ $faker = $this->getFaker(); $this->filePath = $this->getUploadsDirectory() . 'import -' . $numRows . '.csv'; $fh = fopen($this->filePath, "w"); $rows = 'firstname, lastname, username' . PHP_EOL; for ($i = 0; $i < $numRows; $i++) { $firstname = addslashes($faker->firstName); $lastname = addslashes($faker->lastName); $username = $faker->username . $i; //add $i to force unique $rows .= "{$firstname}, {$lastname}, {$username}" . PHP_EOL; } fwrite($fh, $rows); fclose($fh); $repository = $this->getRepository('BatchProcess'); $this->batchProcess = $repository->create()->setFilename($this->filePath); $repository->save($this->batchProcess);}

Page 79: Behat - Beyond the Basics (2016 - SunshinePHP)

The System Has Begun To Process The Spreadsheet/*** @When /^the system has begun to process the spreadsheet$/i*/public function theSystemHasBegunToProcessTheSpreadsheet(){ $command = 'php app' . DIRECTORY_SEPARATOR; $command .= 'console batch:process --batch_id='; $command .= $this->batchProcess->getId();

if (substr(php_uname(), 0, 7) == "Windows") { return pclose(popen("start /B " . $command, "r")); } return exec($command . " > /dev/null &");}

Page 80: Behat - Beyond the Basics (2016 - SunshinePHP)

I Have Waited 1 Minute/*** @When /^I have waited (\d+) minutes?$/*/public function iHaveWaitedSomeMinutes($num){ $seconds = 60; $outputEvery = 30; $cycles = ($num * $seconds) / $outputEvery; for ($i = 0; $i < $cycles; $i++) { sleep($outputEvery); echo '.'; } echo PHP_EOL;}

Page 81: Behat - Beyond the Basics (2016 - SunshinePHP)

The Batch Process Status Should Be/*** @Given /^the batch process status should be set to "(.*)" or "(.*)"$/*/public function theBatchProcessStatusShouldBeSetTo($statusA, $statusB){ $this->getEntityManager()->refresh($this->batchProcess); $statusName = $this->batchProcess->getStatus()->getName(); if ($statusName !== $statusA && $statusName !== $statusB) { throw new \Exception("Status is currently: {$statusName}"); }}

Page 82: Behat - Beyond the Basics (2016 - SunshinePHP)

I should see at least 100 new users/*** @Then /^I should see at least (\d+) new users in the company$/*/public function iShouldSeeAtLeastNewUsersInTheCompany($num){ $company = $this->company; $this->getEntityManager()->refresh($company);

$companyNumUsersNow = $company->getUserCompanies()->count(); $originalNumUsers = $this->companyNumUsers; $difference = ($companyNumUsersNow - $originalNumUsers);

$this->assertGreaterThanOrEqual($num, $difference);}

Page 83: Behat - Beyond the Basics (2016 - SunshinePHP)

Thank You!@jessicamauerhan

@SunshinePHP | 2-5-16 | https://joind.in/talk/9f341

Page 84: Behat - Beyond the Basics (2016 - SunshinePHP)

Resources & ToolsDrivers: http://mink.behat.org/en/latest/guides/drivers.html

Angular AJAX check: https://gist.github.com/jmauerhan/f839926ea527ff5e74e7

Doctrine Data Fixtures: https://github.com/doctrine/data-fixtures

Faker: https://github.com/fzaninotto/Faker

Symfony Finder: http://symfony.com/doc/current/components/finder.html

Page Object Extension: https://github.com/sensiolabs/BehatPageObjectExtension

PHP Zip Archive: http://php.net/manual/en/class.ziparchive.php