PHPSpec & Behat: Two Testing Tools That Write Code For You (#phptek edition)

PHPSpec & Behat: Two Testing Tools That Write Code For You (#phptek edition)

PHPSpec and Behat are two amazing PHP tools that empower specification-driven development and behavior-driven development. These two tools combined can help you build test coverage, but many people don't realize they can also write much of your code for you. In this talk, we'll see what PHPSpec and Behat can do, through a series of examples and use cases. In other words, I heard you like to code, so I wrote code that writes code while you code.

Presented at #phptek - http://tek.phparch.com

8a2e3b7e9a3e4037934e5680e16e53e1?s=128

Joshua Warren

May 21, 2015
Tweet

Transcript

  1. PHPSpec & Behat: Two Testing Tools That Write Code For

    You Presented by Joshua Warren
  2. OR:

  3. None
  4. I heard you like to code, so let’s write code

    that writes code while you code.
  5. About Me

  6. PHP Developer Working with PHP since 1999

  7. Founder & CEO Founded Creatuity in 2008 PHP Development Firm

    Focused on the Magento platform Tink, a Creatuity shareholder
  8. JoshuaWarren.com @JoshuaSWarren

  9. IMPORTANT! • joind.in/13744 • Download slides • Post comments •

    Leave a rating!
  10. What You Need To Know ASSUMPTIONS

  11. Today we assume you’re a PHP developer.

  12. That you are familiar with test driven development.

  13. And that you’ve at least tried PHPUnit, Selenium or another

    testing tool.
  14. BDD - no, the B does not stand for beer,

    despite what a Brit might tell you Behavior Driven Development
  15. Think of BDD as stepping up a level from TDD.

  16. Graphic thanks to BugHuntress

  17. TDD generally deals with functional units.

  18. BDD steps up a level to consider complete features.

  19. In BDD, you write feature files in the form of

    user stories that you test against.
  20. BDD uses a ubiquitous language - basically, a language that

    business stakeholders, project managers, developers and our automated tools can all understand.
  21. Sample Behat Feature File Feature: 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"

  22. BDD gets all stakeholders to agree on what “done” looks

    like before you write a single line of code
  23. Behat

  24. We implement BDD in PHP with a tool called Behat

  25. Behat is a free, open source tool designed for BDD

    and PHP
  26. behat.org

  27. SpecBDD - aka, Testing Tongue Twisters Specification Behavior Driven Development

  28. Before you write a line of code, you write a

    specification for how that code should work
  29. Focuses you on architectural decisions up-front

  30. PHPSpec

  31. Open Source tool for specification driven development in PHP

  32. www.phpspec.net

  33. Why Use Behat and PHPSpec?

  34. These tools allow you to focus exclusively on logic

  35. Helps build functional testing coverage quickly

  36. Guides planning and ensuring that all stakeholders are in agreement

  37. Why Not PHPUnit?

  38. PHPSpec is opinionated - in every sense of the word

  39. PHPSpec forces you to think differently and creates a mindset

    that encourages usage
  40. PHPSpec tests are much more readable

  41. Read any of Marcello Duarte’s slides on testing

  42. What About Performance?

  43. Tests that take days to run won’t be used

  44. PHPSpec is fast

  45. Behat supports parallel execution

  46. Behat and PHPSpec will be at least as fast as

    the existing testing tools, and can be much faster
  47. Enough Theory: Let’s Build Something!

  48. We’ll be building a basic time-off request app.

  49. Visitors can specify their name and a reason for their

    time off request.
  50. Time off requests can be viewed, approved and denied.

  51. Intentionally keeping things simple, but you can follow this pattern

    to add authentication, roles, etc.
  52. Want to follow along or view the sample code?

  53. Vagrant box: https:/ /github.com/joshuaswarren/bdd-box Project code: https:/ /github.com/joshuaswarren/bdd

  54. Setting up Our Project

  55. Setup a folder for your project

  56. Use composer to install Behat, phpspec & friends

  57. composer require behat/behat —dev

  58. composer require behat/mink-goutte-driver —dev

  59. composer require phpspec/phpspec —dev

  60. We now have Behat and Phpspec installed

  61. We also have Mink - an open source browser emulator/controller

  62. Mink Drivers Goutte - headless, fast, no JS Selenium2 -

    requires Selenium server, slower, supports JS Zombie - headless, fast, does support JS
  63. We are using Goutte today because we don’t need Javascript

    support
  64. We’ll perform some basic configuration to let Behat know to

    use Goutte
  65. And we need to let phpspec know where our code

    should go
  66. Run: vendor/bin/behat —init

  67. Create /behat.yml default:
 extensions:
 Behat\MinkExtension:
 base_url: http://192.168.33.10/
 default_session: goutte
 goutte:

    ~

  68. 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
 {
 
 }
  69. Create /phpspec.yml suites:
 app_suites:
 namespace: App
 psr4_prefix: App
 src_path: app


  70. Features

  71. features/UpAndRunning.feature Feature: 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"

  72. Run: bin/behat

  73. features/SubmitTimeOffRequest.feature Feature: 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"

  74. features/SubmitTimeOffRequest.feature Feature: 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"

  75. features/SubmitTimeOffRequest.feature Feature: 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"

  76. features/SubmitTimeOffRequest.feature Feature: 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"

  77. 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
 
 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"
  78. 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
  79. 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"
  80. 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"
  81. run behat: bin/behat

  82. 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)
  83. 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.
  84. None
  85. These failures show us that Behat is testing our app

    properly, and now we just need to write the application logic.
  86. Specifications

  87. Now we write specifications for how our application should work.

  88. These specifications should provide the logic to deliver the results

    that Behat is testing for.
  89. bin/phpspec describe App\\Timeoff

  90. PHPSpec generates a basic spec file for us

  91. spec\TimeoffSpec.php namespace spec\App;
 
 use PhpSpec\ObjectBehavior;
 use Prophecy\Argument;
 
 class

    TimeoffSpec extends ObjectBehavior
 {
 function it_is_initializable()
 {
 $this->shouldHaveType('App\Timeoff');
 }
 }

  92. This default spec tells PHPSpec to expect a class named

    Timeoff.
  93. Now we add a bit more to the file so

    PHPSpec will understand what this class should do.
  94. spec\TimeoffSpec.php function 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);
 }
  95. spec\TimeoffSpec.php function it_creates_timeoff_requests() {
 $this->create("Name", "reason")->shouldBeString();
 }
 
 function it_loads_all_timeoff_requests()

    {
 $this->loadAll()->shouldBeArray();
 }
  96. spec\TimeoffSpec.php function it_loads_a_timeoff_request() {
 $this->load("uuid")->shouldBeArray();
 }
 
 function it_loads_pending_timeoff_requests() {


    $this->loadPending()->shouldBeArray();
 }

  97. spec\TimeoffSpec.php function it_approves_timeoff_requests() {
 $this->approve("id")->shouldReturn(true);
 }
 
 function it_denies_timeoff_requests() {


    $this->deny("id")->shouldReturn(true);
 }
  98. Now we run PHPSpec once more…

  99. Phpspec output 10 ✔ 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.
  100. Lots of failures…

  101. But wait a second - PHPSpec prompts us!

  102. PHPSpec output Do you want me to create `App\Timeoff::create()` for

    you? [Y/n]
  103. PHPSpec will create the class and the methods for us!

  104. This is very powerful with frameworks like Laravel and Magento,

    which have PHPSpec plugins that help PHPSpec know where class files should be located.
  105. And now, the easy part…

  106. Implementation

  107. Implement logic in the new Timeoff class in the locations

    directed by PHPSpec
  108. Implement each function one at a time, running phpspec after

    each one.
  109. spec\TimeoffSpec.php public function create($name, $reason)
 {
 $uuid1 = Uuid::uuid1();
 $uuid

    = $uuid1->toString();
 DB::table('requests')->insert([
 'name' => $name,
 'reason' => $reason,
 'uuid' => $uuid,
 ]);
 return $uuid;
 }
  110. spec\TimeoffSpec.php public function load($uuid) {
 $results = DB::select('select * from

    requests WHERE uuid = ?', [$uuid]);
 return $results;
 }
  111. spec\TimeoffSpec.php public function loadAll()
 {
 $results = DB::select('select * from

    requests');
 return $results;
 }
  112. spec\TimeoffSpec.php public function loadPending()
 {
 $results = DB::select('select * from

    requests WHERE reviewed = ?', [0]);
 return $results;
 }
  113. spec\TimeoffSpec.php public function approve($uuid)
 {
 DB::update('update requests set reviewed =

    1, approved = 1 where uuid = ?', [$uuid]);
 return true;
 }
  114. spec\TimeoffSpec.php public function deny($uuid)
 {
 DB::update('update requests set reviewed =

    1, approved = 0 where uuid = ?', [$uuid]);
 return true;
 }
  115. phpspec should be returning all green

  116. Move on to implementing the front-end behavior

  117. Using Lumen means our view/display logic is very simple

  118. app\Http\route.php $app->get('/bdd/', function() use ($app) {
 return "Welcome to the

    world of BDD";
 });
  119. 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');
 }
 });
  120. 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]);
 }
  121. 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";
 }
 } …
  122. app\Http\route.php …
 } else {
 $request = $to->load($uuid);
 return view('request.manageSpecific',

    ['request' => $request]);
 } …
  123. app\Http\route.php …
 } else {
 $requests = $to->loadAll();
 return view('request.manage',

    ['requests' => $requests]);
 }
  124. Our views are located in resources\views\request\ and are simple HTML

    forms
  125. Once we’re done with the implementation, we move on to…

  126. Testing

  127. Once we’re done, running phpspec run should return green

  128. Once phpspec returns green, run behat, which should return green

    as well
  129. We now know that our new feature is working correctly

    without needing to open a web browser
  130. PHPSpec gives us confidence that the application logic was implemented

    correctly.
  131. Behat gives us confidence that the feature is being displayed

    properly to users.
  132. Running both as we refactor and add new features will

    give us confidence we haven’t broken an existing feature
  133. Success!

  134. Our purpose today was to get you hooked on Behat

    & PHPSpec and show you how easy it is to get started.
  135. Behat and PHPSpec are both powerful tools

  136. PHPSpec can be used at a very granular level to

    ensure your application logic works correctly
  137. Advanced Behat & PHPSpec

  138. I encourage you to learn more about Behat & phpspec.

    Here’s a few areas to consider…
  139. Parallel Execution

  140. A few approaches to running Behat in parallel to improve

    it’s performance. Start with: shvetsgroup/ParallelRunner
  141. Behat - Reusable Actions

  142. “I should see”, “I go to” are just steps -

    you can write your own steps.
  143. Mocking & Prophesying

  144. Mock objects are simulated objects that mimic the behavior of

    real objects
  145. Helpful to mock very complex objects, or objects that you

    don’t want to call while testing - i.e., APIs
  146. Prophecy is a highly opinionated PHP mocking framework by the

    Phpspec team
  147. Take a look at the sample code on Github -

    I mocked a Human Resource Management System API
  148. 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();
  149. PhantomJS

  150. Stick around - Michelle Sanver is up next at 3:30PM

    in this room to discuss Behat + PhantomJS including automated screenshots and screenshot comparision
  151. Two Tasks For You

  152. Next week, setup Behat and PHPSpec on one of your

    projects and take it for a quick test by implementing one short feature.
  153. Keep In Touch! • joind.in/13744 • @JoshuaSWarren • JoshuaWarren.com