Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

123
by your friend: Ryan Weaver @weaverryan Behavioral Driven Development with Behat GrPhpDev FTW Meetup

description

Testing our applications is something we all do. Ahem, rather, it's something we all *wish* we did. In this chat, I'll introduce you to Behat (behat.org) (version 3!!!!): a behavior-driven-development (BDD) library that allows you to write functional tests against your application just by writing human-readable sentences/scenarios. To sweeten the deal these tests can be run in a real browser (via Selenium2) with just the flip of a switch. If you asked me to develop without Behat, I'd just retire. It's that sweet. By the end, you'll have everything you need to start functionally-testing with Behat in your new, or very old and ugly project.

Transcript of Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Page 1: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

by your friend: !

Ryan Weaver @weaverryan

Behavioral Driven Development with Behat

GrPhpDev FTW Meetup

Page 2: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

KnpUniversity.comgithub.com/weaverryan

• Lead for the Symfony documentation!

• KnpLabs US - Symfony consulting, training, Kumbaya!

• Writer for KnpUniversity.comscreencasts

Who is this Hipster?

!

!

!

!

!!

• Husband of the much more talented @leannapelham

Page 3: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Plan, Work, Miscommunicate, Panic, Put out Fires, Repeat!

Intro

(aka Project Work)

Page 4: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

The Typical Project

Page 5: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

How the customer explained it

http://www.projectcartoon.com

Page 6: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

How the project leader understood it

http://www.projectcartoon.com

Page 7: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

How the programmer wrote it

http://www.projectcartoon.com

Page 8: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

What the customer really needed

http://www.projectcartoon.com

Page 9: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

... and my personal favorite

Page 10: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

What the beta testers received

http://www.projectcartoon.com

Page 11: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Computer Science?

https://www.flickr.com/photos/diueine/3604050776

Page 12: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Where it breaks down...

womp womp

Page 13: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Different roles, different languages, miscommunication

@weaverryan

One

Page 14: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

Two

Your code and business values may not align

Page 15: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

I've just dreamt up this cool new feature that we should implement! !

Why? Because it's cool!

Page 16: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

Three

Over-planning, under-planning, planning...?

Page 17: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

https://www.flickr.com/photos/tambako/4175456498

Act 1 !

Getting down with BDD

Page 18: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Evolution of Test-Driven Development

@weaverryan

Page 19: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

“Behaviour” is a more useful word, than “test”

© Dan North, 2003

Page 20: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

Evolution of Test-Driven Development

≈ Unit Tests

≈ Functional Tests

Page 21: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

Specification BDD

http://www.phpspec.net

Page 22: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Scenario-oriented BDD (Story BDD)

Page 23: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

Let’s create a single vocabulary and process

product owners

business analysts

project managers

developers

QA

Page 24: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

product owners

business analysts

project managers

developers

QA

... for planning, implementing, and testing a feature

Page 25: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

product owners

business analysts

project managers

developers

QA

... with a focus on the *behavior* of the feature

Page 26: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

Solution

1. Define business value for the features

2. Prioritize features by their business value

3. Describe them with readable scenarios

4. And only then - implement them

Page 27: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Act 2 !

Gherkin

Page 28: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

A project consists of many features

@weaverryan

Page 29: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

These need to be planned, written and shared

@weaverryan

Page 30: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

Gherkin ==

a structured language to describe a feature

Page 31: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Feature: {custom_title} In order to {A} As a {B} I need to {C}• {A} - the benefit or value of the feature • {B} - the role (or person) who will benefit • {C} - short feature description

| The person “writing” this feature - the “I”

Page 32: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

Solution

1. Define!

2. Prioritize!

3. Describe!

4. And only then -

1. Define business value for the features

Page 33: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Feature: I18n In order to read news in french As a french user I need to be able to switch locale

read news in French

@weaverryan

Page 34: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Feature: I18n In order to read news in french As a french user I need to be able to switch locale

read news in French

The business value

@weaverryan

Page 35: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Feature: I18n In order to read news in french As a french user I need to be able to switch locale

read news in French

The person who benefits +

The “author” of this feature

@weaverryan

Page 36: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Feature: I18n In order to read news in french As a french user I need to be able to switch locale

read news in French

Description of the feature, the action the person will take

@weaverryan

Page 37: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

Solution

1. Define!

2. Prioritize!

3. Describe!

4. And only then -

2. Prioritize features by their business value

Page 38: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Prioritize...

1) Feature: News admin panel!

2) Feature: I18n!

3) Feature: News list API

Page 39: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

1. Define!

2. Prioritize!

3. Describe!

4. And only then -

Solution

3. Describe them with readable scenarios

Page 40: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Feature: News admin panel In order to maintain a list of news As a site administrator I need to be able to edit news

Scenario: Add new article Given I am on the "/admin/news" page When I click "New Article" And I fill in "Title" with "Learned BDD" And I press "Save" Then I should see "A new article was added"

Page 41: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Scenarios

Given !

Defines the initial state of the system for the scenario

Scenario: Add new article Given I am on the "/admin/news" page When I click "New Article" And I fill in "Title" with "Learned BDD" And I press "Save" Then I should see "A new article was added"

Page 42: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Scenarios

When !

Describes the action taken by the person/role

Scenario: Add new article Given I am on the "/admin/news" page When I click "New Article" And I fill in "Title" with "Learned BDD" And I press "Save" Then I should see "A new article was added"

Page 43: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Scenarios

Then !

Describes the observable system state after the action has been performed

Scenario: Add new article Given I am on the "/admin/news" page When I click "New Article" And I fill in "Title" with "Learned BDD" And I press "Save" Then I should see "A new article was added"

Page 44: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Scenarios

And/But !

Can be added to create multiple Given/When/Then lines

Scenario: Add new article Given I am on the "/admin/news" page When I click "New Article" And I fill in "Title" with "Learned BDD" And I press "Save" Then I should see "A new article was added"

Page 45: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Example #2

Scenario: List available articles Given there are 5 news articles And I am on the "/admin" page When I click "News Administration" Then I should see 5 news articles

Page 46: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Gherkin gives us a consistent language

for describing features and their scenarios

Page 47: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

... now let’s turn them into tests!

Page 48: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

http://bit.ly/behatch-t

Act 3

Page 49: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Having a standard way of describing features is cool...

Page 50: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

... executing those sentences as functional tests is just

*AWESOME*

Page 51: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

What is Behat?

Behat does one simple thing:

** each line in a scenario is called a “step”

Behat “executes” your scenarios, reading each step and calling the function associated with it

It Maps Each step** to a PHP Callback

Page 52: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Installing Behat

Behat is just a library that can be installed easily in any

project via Composer

New to Composer? Free screencast cures it! KnpUniversity.com/screencast/composer

Page 53: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

In your project directory...

1) Download Composer

$> curl -s http://getcomposer.org/installer | php

2) Create (or update) composer.json for Behat

bit.ly/behat3-composer!

Page 54: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

{ "require": { "behat/behat": "~3.0" }}

bit.ly/behat3-composer

Page 55: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

In your project directory...

1) Download Composer

$> curl -s http://getcomposer.org/installer | php

2) Create (or update) composer.json for Behat

http://bit.ly/behat3-composer

3) Download Behat libraries

$> php composer.phar update

Page 56: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

\o/ Woo!

Page 57: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

The most important product of the installation is an

executable vendor/bin/behat file

Page 58: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat
Page 59: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

To use Behat in a project you need: !

!

1) Actual *.feature files to be executed

2) A FeatureContext.php file that holds the PHP callbacks for each step

3) (optional) A behat.yml configuration file

Page 60: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

$> php vendor/bin/behat --init

Page 61: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

<?php// features/bootstrap/FeatureContext.php use Behat\Behat\Context\SnippetAcceptingContext; use Behat\Gherkin\Node\PyStringNode; use Behat\Gherkin\Node\TableNode; /** * Behat context class. */ class FeatureContext implements SnippetAcceptingContext { }

Page 62: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

<?php// features/bootstrap/FeatureContext.php use Behat\Behat\Context\SnippetAcceptingContext; use Behat\Gherkin\Node\PyStringNode; use Behat\Gherkin\Node\TableNode; /** * Behat context class. */ class FeatureContext implements SnippetAcceptingContext { }

Nothing Interesting here yet...

Page 63: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Pretend you’re testing the “ls” program

Page 64: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

1) Describe your Feature

Feature: ls

features/ls.feature

In order to see the directory structure As a UNIX user I need to be able to list the current directory's contents

Page 65: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

2) Your First Scenario

“If you have two files in a directory, and you're running the command -

you should see them listed.”

Page 66: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Scenario: List 2 files in a directory

Write in the natural voice of “a UNIX user”

Given I have a file named "foo" And I have a file named "bar" When I run "ls" Then I should see "foo" in the output And I should see "bar" in the output

features/ls.feature

Page 67: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

3) Run Behat

$> php vendor/bin/behat

Page 68: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Behat tries to find a

method in FeatureContext for each step

Page 69: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Matching is done with

simple wildcards

For each step that doesn’t have a matching

method, Behat prints code to copy into FeatureContext

Page 70: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

class FeatureContext extends BehatContext{ /** @Given I have a file named :file */ public function iHaveAFileNamed($file) { throw new PendingException(); } /** @When I run :command */ public function iRun($command) { throw new PendingException(); } // ...}

4) Copy in the new Definitions

Quoted text maps to a method

argument

Page 71: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

5) Make the definitions do what they need to

Page 72: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

/** * @Given I have a file named :file */public function iHaveAFileNamed($file) { touch($file);}!

/** * @Given I have a directory named :dir */public function iHaveADirectoryNamed($dir) { mkdir($dir);}

Page 73: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

/** * @When I run :command */public function iRun($command) { exec($command, $output); $this->output = trim(implode("\n", $output));}!/** * @Then I should see :string in the output */public function iShouldSeeInTheOutput($string) { if (strpos($this->output, $string) === false) { throw new \Exception(‘Did not see’.$string); );}

Page 74: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat
Page 75: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat
Page 76: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat
Page 77: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat
Page 78: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

See the full FeatureContext class:

!

http://bit.ly/behat-ls-feature

Page 79: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Scenario Step

Definition

Given I have a file named “foo”

pattern

public function iHaveAFileNamed($file) {

do work

Pass/Fail: Each step is a “test”, which passes *unless* an exception is thrown

touch($file);

@Given I have a file named :file

What Behat *does*

Page 80: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Creating files and directories in FeatureContext is nice...

Page 81: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

but wouldn’t it be really cool to command a browser, fill out forms and check the output?

Page 82: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Act 4 !

Mink

https://www.flickr.com/photos/15016964@N02/5696367600

Page 83: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Mink!

http://mink.behat.org/

• A standalone library to use PHP to command a “browser”

• One easy API that can be used to command Selenium, Goutte, ZombieJS, etc

@weaverryan

Page 84: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

A sample of Mink

Page 85: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

use Behat\Mink\Driver\GoutteDriver;use Behat\Mink\Session;!

// change *only* this line to run// in Selenium, etc$driver = new GoutteDriver();$session = new Session($driver);

Page 86: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

// visit a page$session->visit('http://behat.org');

echo 'URL : '.$session->getCurrentUrl();

echo 'Status: '.$session->getStatusCode();

Page 87: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

$page = $session->getPage();

// drill down into the page$ele = $page->find('css', 'li:nth-child(4) a');

echo 'Link text is: '.$ele->getText();echo 'href is: '.$ele->getAttribute('href');

// click the link// (you can also fill out forms)$ele->click();

Page 88: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Mink inside FeatureContext =>

Dangerous Combo for Functional Testing

Page 89: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

http://mink.behat.org/@weaverryan

Behat Mink

Integration

MinkExtension

• An “Extension” is like a Behat plugin

• The MinkExtension makes using Mink inside Behat a matter of configuration

Page 90: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

Install Mink & MinkExtension

$> php composer.phar update

• Update composer.json to include > Mink

> MinkExtension > Goutte and Selenium2 Drivers for Mink

http://bit.ly/behat-mink-composer

• Update the vendor libraries

Page 91: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

{ "require": { "behat/behat": "~3.0", "behat/mink-extension": "~2.0@dev", "behat/mink-goutte-driver": "~1.0", "behat/mink-selenium2-driver": "~1.1" }}

http://bit.ly/behat-mink-composer

Page 92: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Goal: !

To easily use Mink inside FeatureContext

Page 93: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

Bootstrap MinkExtension

behat.yml is the Behat configuration file and can contain much more than you see here

# behat.yml default: extensions: Behat\MinkExtension: goutte: ~ selenium2: ~ # The base URL you're testing base_url: http://en.wikipedia.org/

http://bit.ly/behat-yml

Page 94: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

Extend MinkContext

use Behat\MinkExtension\Context\RawMinkContext; /** * Behat context class. */ class FeatureContext extends RawMinkContext

Extending RawMinkContext gives us...

Page 95: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Access to a Mink Sessionclass FeatureContext extends RawMinkContext{ public function doSomething() { $session = $this->getSession(); $session->visit('http://behat.org'); }!

// ...}

Our custom definitions can now command a browser!

Page 96: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

More! Add MinkContext# behat.yml default: extensions: # ... suites: default: contexts: - FeatureContext - Behat\MinkExtension\Context\MinkContext

Behat now parses definitions from *our* class *and* this MinkContext class

Page 97: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

We inherit a pile of great definitions

the -dl option prints all current definitions

Before adding MinkContext:

Page 98: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

After adding MinkContext:

Page 99: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

In other words: We can write some tests for our app without writing any

PHP code

Page 100: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Suppose we’re testing Wikipedia.org

Page 101: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

# features/wikipedia.featureFeature: Search In order to see a word definition As a website user I need to be able to search for a word

These 4 definitions all come packaged with MinkContext

Scenario: Searching for a page that does exist Given I am on "/wiki/Main_Page" When I fill in "search" with "Behavior Driven Development" And I press "searchButton" Then I should see "agile software development"

Page 102: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Celebration!

Page 103: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Act 5 !

JavaScript

https://www.flickr.com/photos/yoanngd/10669976224

Page 104: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Let’s try another scenario !

Auto-Completion!

Page 105: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat
Page 106: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

# features/wikipedia.feature

Scenario: Searching for a page with autocompletion Given I am on "/wiki/Main_Page" When I fill in "search" with "Behavior Driv" Then I should see "Behavior Driven Development"

Page 107: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat
Page 108: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

http://en.wikipedia.org/wiki/The_Scream

Page 109: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Can we run this so that JavaScript works?

Page 110: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Add @javascript

# ...!@javascript Scenario: Searching for a page with autocompletion Given I am on "/wiki/Main_Page" When I fill in "search" with "Behavior Driv" Then I should see "Behavior Driven Development”!!!

Page 111: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Add @javascript

# ...!@javascript Scenario: Searching for a page with autocompletion Given I am on "/wiki/Main_Page" When I fill in "search" with "Behavior Driv" Then I should see "Behavior Driven Development”!!!

Yep, that’s all you do!

Page 112: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Download and start Selenium

$> wget http://selenium-release.storage.googleapis.com/2.42/selenium-server-standalone-2.42.2.jar!!$> java -jar selenium-server-standalone-2.42.2.jar

Page 113: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Re-run the tests

Page 114: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Yes, add only 1 line of code to run a test in Selenium

Page 115: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

(you can also use phantomjs)

Page 116: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Your Turn!

https://www.flickr.com/photos/dtelegraph/5907116936

Page 117: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

0) Decide Behat 2 or 3

* Behat 3 is stable, and documentationis decent (install + config are the bigdifferences)

!

* Behat 2 is stable, long-term supported and not too different from 3

Page 118: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

1) Install Behat (v2)

http://knpuniversity.com/screencast/behat

Page 119: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

... and learn more about what you can do with Mink: http://mink.behat.org/

2) Write features for your app!

Page 120: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

3) high-five your teammates

https://www.flickr.com/photos/nickwebb/3904325807

Page 121: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

GrPhpDev FTW Meetup

Thanks!

Ryan Weaver @weaverryan

KnpUniversity.com PHP, Behat, Twig, OO, etc Tutorial Screencasts

Page 122: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

Ryan Weaver @weaverryan

... and we love west MI!

Page 123: Grand Rapids PHP Meetup: Behavioral Driven Development with Behat

@weaverryan

KnpUniversity.com