pnwphp - PHPSpec & Behat: Two Testing Tools That Write Code For You
-
Upload
joshua-warren -
Category
Software
-
view
379 -
download
2
Transcript of pnwphp - PHPSpec & Behat: Two Testing Tools That Write Code For You
PHPSpec & Behat: Two Testing Tools That Write Code For You
Presented by Joshua Warren
OR:
I heard you like to code, so let’s write code that writes
code while you code.
About Me
PHP DeveloperWorking with PHP since 1999
Founder & CEOFounded Creatuity in 2008
PHP Development Firm
Focused on the Magento platform
JoshuaWarren.com
@JoshuaSWarren
IMPORTANT!
• joind.in/14919
• Download slides
• Post comments
• Leave a rating!
What You Need To Know
ASSUMPTIONS
Today we assume you’re a PHP developer.
That you are familiar with test driven development.
And that you’ve at least tried PHPUnit, Selenium or another testing tool.
BDD - no, the B does not stand for beer, despite what a Brit might tell you
Behavior Driven Development
Think of BDD as stepping up a level from TDD.
Graphic thanks to BugHuntress
TDD generally deals with functional units.
BDD steps up a level to consider complete features.
In BDD, you write feature files in the form of user stories that you test against.
BDD uses a ubiquitous language - basically, a language that business stakeholders, project
managers, developers and our automated tools can all understand.
Sample Behat Feature FileFeature: Up and Running In order to confirm Behat is Working As a developer I need to see a homepage Scenario: Homepage Exists When I go to "/bdd/" Then I should see "Welcome to the world of BDD"
BDD gets all stakeholders to agree on what “done” looks like before you write a single line of code
Behat
We implement BDD in PHP with a tool called Behat
Behat is a free, open source tool designed for BDD and PHP
behat.org
SpecBDD - aka, Testing Tongue Twisters
Specification Behavior Driven Development
Before you write a line of code, you write a specification for how that code should work
Focuses you on architectural decisions up-front
PHPSpec
Open Source tool for specification driven development in PHP
www.phpspec.net
Why Use Behat and PHPSpec?
These tools allow you to focus exclusively on logic
Helps build functional testing coverage quickly
Guides planning and ensuring that all stakeholders are in agreement
Why Not PHPUnit?
PHPSpec is opinionated - in every sense of the word
PHPSpec forces you to think differently and creates a mindset that encourages usage
PHPSpec tests are much more readable
Read any of Marcello Duarte’s slides on testing
What About Performance?
Tests that take days to run won’t be used
PHPSpec is fast
Behat supports parallel execution
Behat and PHPSpec will be at least as fast as the existing testing tools, and can be much faster
Enough Theory:Let’s Build Something!
We’ll be building a basic time-off request app.
Visitors can specify their name and a reason for their time off request.
Time off requests can be viewed, approved and denied.
Intentionally keeping things simple, but you can follow this pattern to add authentication,
roles, etc.
Want to follow along or view the sample code?
Vagrant box:
https://github.com/joshuaswarren/bdd-box
Project code:
https://github.com/joshuaswarren/bdd
Setting up Our Project
Setup a folder for your project
Use composer to install Behat, phpspec & friends
composer require behat/behat —dev
composer require behat/mink-goutte-driver —dev
composer require phpspec/phpspec —dev
We now have Behat and Phpspec installed
We also have Mink - an open source browser emulator/controller
Mink Drivers
Goutte - headless, fast, no JS
Selenium2 - requires Selenium server, slower, supports JS
Zombie - headless, fast, does support JS
We are using Goutte today because we don’t need Javascript support
We’ll perform some basic configuration to let Behat know to use Goutte
And we need to let phpspec know where our code should go
Run:
vendor/bin/behat —init
Create /behat.yml
default: extensions: Behat\MinkExtension: base_url: http://192.168.33.10/ default_session: goutte goutte: ~
features/bootstrap/FeatureContext.php
use Behat\Behat\Context\Context;use Behat\Behat\Context\SnippetAcceptingContext;use Behat\Gherkin\Node\PyStringNode;use Behat\Gherkin\Node\TableNode;use Behat\MinkExtension\Context\MinkContext;/** * Defines application features from the specific context. */class FeatureContext extends Behat\MinkExtension\Context\MinkContext {}
Create /phpspec.yml
suites: app_suites: namespace: App psr4_prefix: App src_path: app
Features
features/UpAndRunning.featureFeature: Up and Running In order to confirm Behat is Working As a developer I need to see a homepage Scenario: Homepage Exists When I go to "/bdd/" Then I should see "Welcome to the world of BDD"
Run:
bin/behat
features/SubmitTimeOffRequest.featureFeature: Submit Time Off Request In order to request time off As a developer I need to be able to fill out a time off request form Scenario: Time Off Request Form Exists When I go to "/bdd/timeoff/new" Then I should see "New Time Off Request" Scenario: Time Off Request Form Works When I go to "/bdd/timeoff/new" And I fill in "name" with "Josh" And I fill in "reason" with "Attending a great conference" And I press "submit" Then I should see "Time Off Request Submitted"
features/SubmitTimeOffRequest.featureFeature: Submit Time Off Request In order to request time off As a developer I need to be able to fill out a time off request form Scenario: Time Off Request Form Exists When I go to "/bdd/timeoff/new" Then I should see "New Time Off Request" Scenario: Time Off Request Form Works When I go to "/bdd/timeoff/new" And I fill in "name" with "Josh" And I fill in "reason" with "Attending a great conference" And I press "submit" Then I should see "Time Off Request Submitted"
features/SubmitTimeOffRequest.featureFeature: Submit Time Off Request In order to request time off As a developer I need to be able to fill out a time off request form Scenario: Time Off Request Form Exists When I go to "/bdd/timeoff/new" Then I should see "New Time Off Request" Scenario: Time Off Request Form Works When I go to "/bdd/timeoff/new" And I fill in "name" with "Josh" And I fill in "reason" with "Attending a great conference" And I press "submit" Then I should see "Time Off Request Submitted"
features/SubmitTimeOffRequest.featureFeature: Submit Time Off Request In order to request time off As a developer I need to be able to fill out a time off request form Scenario: Time Off Request Form Exists When I go to "/bdd/timeoff/new" Then I should see "New Time Off Request" Scenario: Time Off Request Form Works When I go to "/bdd/timeoff/new" And I fill in "name" with "Josh" And I fill in "reason" with "Attending a great conference" And I press "submit" Then I should see "Time Off Request Submitted"
features/ProcessTimeOffRequest.featureFeature: Process Time Off Request In order to manage my team As a manager I need to be able to approve and deny time off requests Scenario: Time Off Request Management View Exists When I go to "/bdd/timeoff/manage" Then I should see "Manage Time Off Requests" Scenario: Time Off Request List When I go to "/bdd/timeoff/manage" And I press "View" Then I should see "Pending Time Off Request Details" Scenario: Approve Time Off Request When I go to "/bdd/timeoff/manage" And I press "View" And I press "Approve" Then I should see "Time Off Request Approved" Scenario: Deny Time Off Request When I go to "/bdd/timeoff/manage" And I press "View" And I press "Deny" Then I should see "Time Off Request Denied"
features/ProcessTimeOffRequest.feature
Feature: Process Time Off Request In order to manage my team As a manager I need to be able to approve and deny time off requests
features/ProcessTimeOffRequest.feature
Scenario: Time Off Request Management View Exists When I go to "/bdd/timeoff/manage" Then I should see "Manage Time Off Requests" Scenario: Time Off Request List When I go to "/bdd/timeoff/manage" And I press "View" Then I should see "Pending Time Off Request Details"
features/ProcessTimeOffRequest.feature
Scenario: Approve Time Off Request When I go to "/bdd/timeoff/manage" And I press "View" And I press "Approve" Then I should see "Time Off Request Approved" Scenario: Deny Time Off Request When I go to "/bdd/timeoff/manage" And I press "View" And I press "Deny" Then I should see "Time Off Request Denied"
run behat: bin/behat
Behat Output--- Failed scenarios:
features/ProcessTimeOffRequest.feature:6
features/ProcessTimeOffRequest.feature:10
features/ProcessTimeOffRequest.feature:15
features/ProcessTimeOffRequest.feature:21
features/SubmitTimeOffRequest.feature:6
features/SubmitTimeOffRequest.feature:10
7 scenarios (1 passed, 6 failed)
22 steps (8 passed, 6 failed, 8 skipped)
0m0.61s (14.81Mb)
Behat Output
Scenario: Time Off Request Management View Exists
When I go to “/bdd/timeoff/manage"
Then I should see "Manage Time Off Requests"
The text "Manage Time Off Requests" was not found anywhere in the text of the current page.
These failures show us that Behat is testing our app properly, and now we just need to
write the application logic.
Specifications
Now we write specifications for how our application should work.
These specifications should provide the logic to deliver the results that Behat is testing for.
bin/phpspec describe App\\Timeoff
PHPSpec generates a basic spec file for us
spec\TimeoffSpec.phpnamespace spec\App;use PhpSpec\ObjectBehavior;use Prophecy\Argument;class TimeoffSpec extends ObjectBehavior{ function it_is_initializable() { $this->shouldHaveType('App\Timeoff'); }}
This default spec tells PHPSpec to expect a class named Timeoff.
Now we add a bit more to the file so PHPSpec will understand what this class should do.
spec\TimeoffSpec.phpfunction it_creates_timeoff_requests() { $this->create("Name", "reason")->shouldBeString();}function it_loads_all_timeoff_requests() { $this->loadAll()->shouldBeArray();}function it_loads_a_timeoff_request() { $this->load("uuid")->shouldBeArray();}function it_loads_pending_timeoff_requests() { $this->loadPending()->shouldBeArray();}function it_approves_timeoff_requests() { $this->approve("id")->shouldReturn(true);}function it_denies_timeoff_requests() { $this->deny("id")->shouldReturn(true);}
spec\TimeoffSpec.php
function it_creates_timeoff_requests() { $this->create("Name", "reason")->shouldBeString();}function it_loads_all_timeoff_requests() { $this->loadAll()->shouldBeArray();}
spec\TimeoffSpec.php
function it_loads_a_timeoff_request() { $this->load("uuid")->shouldBeArray();}function it_loads_pending_timeoff_requests() { $this->loadPending()->shouldBeArray();}
spec\TimeoffSpec.php
function it_approves_timeoff_requests() { $this->approve("id")->shouldReturn(true);}function it_denies_timeoff_requests() { $this->deny("id")->shouldReturn(true);}
Now we run PHPSpec once more…
Phpspec output10 ✔ is initializable
15 ! creates timeoff requests
method App\Timeoff::create not found.
19 ! loads all timeoff requests
method App\Timeoff::loadAll not found.
23 ! loads pending timeoff requests
method App\Timeoff::loadPending not found.
27 ! approves timeoff requests
method App\Timeoff::approve not found.
31 ! denies timeoff requests
method App\Timeoff::deny not found.
Lots of failures…
But wait a second - PHPSpec prompts us!
PHPSpec output
Do you want me to create `App\Timeoff::create()` for you?
[Y/n]
PHPSpec will create the class and the methods for us!
This is very powerful with frameworks like Laravel and Magento, which have PHPSpec plugins that help
PHPSpec know where class files should be located.
And now, the easy part…
Implementation
Implement logic in the new Timeoff class in the locations directed by PHPSpec
Implement each function one at a time, running phpspec after each one.
spec\TimeoffSpec.phppublic function create($name, $reason){ $uuid1 = Uuid::uuid1(); $uuid = $uuid1->toString(); DB::table('requests')->insert([ 'name' => $name, 'reason' => $reason, 'uuid' => $uuid, ]); return $uuid;}
spec\TimeoffSpec.php
public function load($uuid) { $results = DB::select('select * from requests WHERE uuid = ?', [$uuid]); return $results;}
spec\TimeoffSpec.php
public function loadAll(){ $results = DB::select('select * from requests'); return $results;}
spec\TimeoffSpec.php
public function loadPending(){ $results = DB::select('select * from requests WHERE reviewed = ?', [0]); return $results;}
spec\TimeoffSpec.php
public function approve($uuid){ DB::update('update requests set reviewed = 1, approved = 1 where uuid = ?', [$uuid]); return true;}
spec\TimeoffSpec.php
public function deny($uuid){ DB::update('update requests set reviewed = 1, approved = 0 where uuid = ?', [$uuid]); return true;}
phpspec should be returning all green
Move on to implementing the front-end behavior
Using Lumen means our view/display logic is very simple
app\Http\route.php
$app->get('/bdd/', function() use ($app) { return "Welcome to the world of BDD";});
app\Http\route.php$app->get('/bdd/timeoff/new/', function() use ($app) { if(Request::has('name')) { $to = new \App\Timeoff(); $name = Request::input('name'); $reason = Request::input('reason'); $to->create($name, $reason); return "Time off request submitted"; } else { return view('request.new'); }});
app\Http\route.php$app->get('/bdd/timeoff/manage/', function() use ($app) { $to = new \App\Timeoff(); if(Request::has('uuid')) { $uuid = Request::input('uuid'); if(Request::has('process')) { $process = Request::input('process'); if($process == 'approve') { $to->approve($uuid); return "Time Off Request Approved"; } else { if($process == 'deny') { $to->deny($uuid); return "Time Off Request Denied"; } } } else { $request = $to->load($uuid); return view('request.manageSpecific', ['request' => $request]); } } else { $requests = $to->loadAll(); return view('request.manage', ['requests' => $requests]); }
app\Http\route.php$app->get('/bdd/timeoff/manage/', function() use ($app) { $to = new \App\Timeoff(); if(Request::has('uuid')) { $uuid = Request::input('uuid'); if(Request::has('process')) { $process = Request::input('process'); if($process == 'approve') { $to->approve($uuid); return "Time Off Request Approved"; } else { if($process == 'deny') { $to->deny($uuid); return "Time Off Request Denied"; } }
…
app\Http\route.php
… } else { $request = $to->load($uuid); return view('request.manageSpecific', ['request' => $request]); }
…
app\Http\route.php
… } else { $requests = $to->loadAll(); return view('request.manage', ['requests' => $requests]); }
Our views are located in resources\views\request\ and are simple HTML forms
Once we’re done with the implementation, we move on to…
Testing
Once we’re done, running phpspec run should return green
Once phpspec returns green, run behat, which should return green as well
We now know that our new feature is working correctly without needing to open a web
browser
This allows us to flow from function to function as we implement our app, without
breaking our train of thought.
PHPSpec gives us confidence that the application logic was implemented correctly.
Behat gives us confidence that the feature is being displayed properly to users.
Running both as we refactor and add new features will give us confidence we haven’t
broken an existing feature
Success!
Our purpose today was to get you hooked on Behat & PHPSpec and show you how easy it is
to get started.
Behat and PHPSpec are both powerful tools
PHPSpec can be used at a very granular level to ensure your application logic works
correctly
Advanced Behat & PHPSpec
I encourage you to learn more about Behat & phpspec. Here’s a few areas to consider…
Parallel Execution
A few approaches to running Behat in parallel to improve it’s performance. Start with:
shvetsgroup/ParallelRunner
Behat - Reusable Actions
“I should see”, “I go to” are just steps - you can write your own steps.
Mocking & Prophesying
Mock objects are simulated objects that mimic the behavior of real objects
Helpful to mock very complex objects, or objects that you don’t want to call while
testing - i.e., APIs
Prophecy is a highly opinionated PHP mocking framework by the Phpspec team
Take a look at the sample code on Github - I mocked a Human Resource Management
System API
Mocking with Prophecy$this->prophet = new \Prophecy\Prophet;
$prophecy = $this->prophet->prophesize('App\HrmsApi');
$prophecy->getUser(Argument::type('string'))->willReturn('name');
$prophecy->decrement('name', Argument::type('integer'))->willReturn(true);
$dummyApi = $prophecy->reveal();
PhantomJS
Can use PhantomJS with Behat to render Javascript, including automated screenshots
and screenshot comparison
Two Tasks For You
Next week, setup Behat and PHPSpec on one of your projects and take it for a quick test by
implementing one short feature.