PHPSpec & Behat: Two Testing Tools That Write Code For You (#phptek edition)
-
Upload
joshua-warren -
Category
Technology
-
view
137 -
download
0
Transcript of PHPSpec & Behat: Two Testing Tools That Write Code For You (#phptek edition)
Founder & CEOFounded Creatuity in 2008
PHP Development Firm
Focused on the Magento platform Tink, a Creatuity shareholder
BDD - no, the B does not stand for beer, despite what a Brit might tell you
Behavior Driven Development
Graphic thanks to BugHuntress
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"
Read any of Marcello Duarte’s slides on testing
Intentionally keeping things simple, but you can follow this pattern to add authentication,
roles, etc.
Vagrant box:
https://github.com/joshuaswarren/bdd-box
Project code:
https://github.com/joshuaswarren/bdd
Mink Drivers
Goutte - headless, fast, no JS
Selenium2 - requires Selenium server, slower, supports JS
Zombie - headless, fast, does support JS
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 {}
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"
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"
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.
spec\TimeoffSpec.phpnamespace spec\App;use PhpSpec\ObjectBehavior;use Prophecy\Argument;class TimeoffSpec extends ObjectBehavior{ function it_is_initializable() { $this->shouldHaveType('App\Timeoff'); }}
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);}
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.
This is very powerful with frameworks like Laravel and Magento, which have PHPSpec plugins that help
PHPSpec know where class files should be located.
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;}
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]); }
Running both as we refactor and add new features will give us confidence we haven’t
broken an existing feature
Our purpose today was to get you hooked on Behat & PHPSpec and show you how easy it is
to get started.
A few approaches to running Behat in parallel to improve it’s performance. Start with:
shvetsgroup/ParallelRunner
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
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();
Stick around - Michelle Sanver is up next at 3:30PM in this room to discuss Behat +
PhantomJS including automated screenshots and screenshot comparision
Next week, setup Behat and PHPSpec on one of your projects and take it for a quick test by
implementing one short feature.