Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Behat for characterization tests

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

Behat for characterization tests

Once an API ships it doesn’t matter how it should behave – how it actually behaves is the important part. Users depend on the existing behaviour and we need a way to ensure that it doesn’t change Behat is a tool that was built to help design software, but it’s actually a great tool for capturing existing behaviour too. We’ve used these tools to gain confidence to refactor 5+ year old apps by capturing the existing behaviour before making changes. I want to share the secrets we learned with you.

Avatar for Michael Heap

Michael Heap

May 20, 2018
Tweet

More Decks by Michael Heap

Other Decks in Technology

Transcript

  1. @mheap #phpkonf $$$ $$$ $$$ Billing Rating Auth Report Masking

    Encoding public function provides() { return [Client ::class]; }
  2. @mheap https://twitter.com/brianwisti/status/503987766032494592 YOU ARE IN A LEGACY CODEBASE > RUN

    TESTS YOU HAVE NO TESTS > READ SPEC YOU HAVE NO SPEC > WRITE FIX YOU ARE EATEN BY AN ELDER CODE HACK.
  3. @mheap Legacy Code is “valuable code that we feel afraid

    to change” JB Rainsberger - Surviving Legacy Code with Golden Master and Sampling
  4. @mheap #phpkonf class SalesUtil { const BQ = 1000.0; const

    BCR = 0.20; const OQM1 = 1.5; const OQM2 = self ::OQM1 * 2; static public function calculate($tSales) { if ($tSales <= static ::BQ) { return $tSales * static ::BCR; } else if ($tSales <= static ::BQ*2) { return static ::BQ * static ::BCR + ($tSales - static ::BQ) * static ::BCR * static ::OQM1; } else { return static ::BQ * static ::BCR + ($tSales - static ::BQ) * static ::BCR * static ::OQM1 + ($tSales - static ::BQ * 2) * static ::BCR * static ::OQM2; } } }
  5. @mheap #phpkonf class SalesUtil { const BQ = 1000.0; const

    BCR = 0.20; const OQM1 = 1.5; const OQM2 = self ::OQM1 * 2; static public function calculate($tSales) { if ($tSales <= static ::BQ) { return $tSales * static ::BCR; } else if ($tSales <= static ::BQ*2) { return static ::BQ * static ::BCR + ($tSales - static ::BQ) * static ::BCR * static ::OQM1; } else { return static ::BQ * static ::BCR + ($tSales - static ::BQ) * static ::BCR * static ::OQM1 + ($tSales - static ::BQ * 2) * static ::BCR * static ::OQM2; } } }
  6. @mheap #phpkonf class SalesUtil { const BQ = 1000.0; const

    BCR = 0.20; const OQM1 = 1.5; const OQM2 = self ::OQM1 * 2; static public function calculate($tSales) { if ($tSales <= static ::BQ) { return $tSales * static ::BCR; } else if ($tSales <= static ::BQ*2) { return static ::BQ * static ::BCR + ($tSales - static ::BQ) * static ::BCR * static ::OQM1; } else { return static ::BQ * static ::BCR + ($tSales - static ::BQ) * static ::BCR * static ::OQM1 + ($tSales - static ::BQ * 2) * static ::BCR * static ::OQM2; } } }
  7. @mheap #phpkonf class SalesUtil { const BQ = 1000.0; const

    BCR = 0.20; const OQM1 = 1.5; const OQM2 = self ::OQM1 * 2; static public function calculate($tSales) { if ($tSales <= static ::BQ) { return $tSales * static ::BCR; } else if ($tSales <= static ::BQ*2) { return static ::BQ * static ::BCR + ($tSales - static ::BQ) * static ::BCR * static ::OQM1; } else { return static ::BQ * static ::BCR + ($tSales - static ::BQ) * static ::BCR * static ::OQM1 + ($tSales - static ::BQ * 2) * static ::BCR * static ::OQM2; } } }
  8. @mheap #phpkonf use PHPUnit\Framework\TestCase; class SalesUtilTest extends TestCase { public

    function testX() { $this ->assertEquals(null, SalesUtil ::calculate(1000)); } }
  9. @mheap #phpkonf use PHPUnit\Framework\TestCase; class SalesUtilTest extends TestCase { public

    function testX() { $this ->assertEquals(null, SalesUtil ::calculate(999)); } }
  10. @mheap #phpkonf use PHPUnit\Framework\TestCase; class SalesUtilTest extends TestCase { public

    function testX() { $this ->assertEquals(null, SalesUtil ::calculate(1000)); } }
  11. @mheap #phpkonf There was 1 failure: 1) SalesUtilTest ::testX Failed

    asserting that 200 matches expected null. /Users/michael/development/oss/characterisation-tests-examples/sales-util/test/SalesUtilTest.php:8 FAILURES! Tests: 1, Assertions: 1, Failures: 1.
  12. @mheap #phpkonf use PHPUnit\Framework\TestCase; class SalesUtilTest extends TestCase { public

    function testSalesLTE1000Pay20PercentCommission() { $this ->assertEquals(200, SalesUtil ::calculate(1000)); } }
  13. @mheap #phpkonf class SalesUtil { const BQ = 1000.0; const

    BCR = 0.20; const OQM1 = 1.5; const OQM2 = self ::OQM1 * 2; static public function calculate($tSales) { if ($tSales <= static ::BQ) { return $tSales * static ::BCR; } else if ($tSales <= static ::BQ*2) { return static ::BQ * static ::BCR + ($tSales - static ::BQ) * static ::BCR * static ::OQM1; } else { return static ::BQ * static ::BCR + ($tSales - static ::BQ) * static ::BCR * static ::OQM1 + ($tSales - static ::BQ * 2) * static ::BCR * static ::OQM2; } } }
  14. @mheap #phpkonf There was 1 failure: 1) SalesUtilTest ::testX Failed

    asserting that 500.0 matches expected null. /Users/michael/development/oss/characterisation-tests-examples/sales-util/test/SalesUtilTest.php:13 FAILURES! Tests: 2, Assertions: 2, Failures: 1.
  15. @mheap #phpkonf public function test2000Gets500Commission() { $this ->assertEquals(500, SalesUtil ::calculate(2000));

    } public function testX() { $this ->assertEquals(null, SalesUtil ::calculate(1600)); }
  16. @mheap #phpkonf There was 1 failure: 1) SalesUtilTest ::testX Failed

    asserting that 380.0 matches expected null. /Users/michael/development/oss/characterisation-tests-examples/sales-util/test/SalesUtilTest.php:19 FAILURES! Tests: 3, Assertions: 3, Failures: 1.
  17. @mheap #phpkonf class SalesUtil { const BQ = 1000.0; const

    BCR = 0.20; const OQM1 = 1.5; const OQM2 = self ::OQM1 * 2; static public function calculate($tSales) { if ($tSales <= static ::BQ) { return $tSales * static ::BCR; } else if ($tSales <= static ::BQ*2) { return static ::BQ * static ::BCR + ($tSales - static ::BQ) * static ::BCR * static ::OQM1; } else { return static ::BQ * static ::BCR + ($tSales - static ::BQ) * static ::BCR * static ::OQM1 + ($tSales - static ::BQ * 2) * static ::BCR * static ::OQM2; } } }
  18. @mheap #phpkonf Input Expected 0 0 10 2 500 100

    1000 200 1001 200.3 1500 350 1982 494.6 1999.9 499.97 2000 500 2001 500.9 5000 3200 7890 5801 10000 7700
  19. @mheap #phpkonf Given I have 1 "Red Bucket" in my

    basket And I have 2 "Large Spades" in my basket When I add 1 "Red Bucket" in my basket Then I should see 2 "Red Buckets" in my basket And I should see 2 "Large Spades" in my basket
  20. @mheap #phpkonf Given I have 1 "Red Bucket" in my

    basket And I have 2 "Large Spades" in my basket When I add 1 "Red Bucket" in my basket Then I should see 2 "Red Buckets" in my basket And I should see 2 "Large Spades" in my basket
  21. @mheap #phpkonf Given I have 1 "Red Bucket" in my

    basket And I have 2 "Large Spades" in my basket When I add 1 "Red Bucket" in my basket Then I should see 2 "Red Buckets" in my basket And I should see 2 "Large Spades" in my basket
  22. @mheap #phpkonf Given I have 1 "Red Bucket" in my

    basket And I have 2 "Large Spades" in my basket When I add 1 "Red Bucket" in my basket Then I should see 2 "Red Buckets" in my basket And I should see 2 "Large Spades" in my basket
  23. @mheap #phpkonf Given I have 1 "Red Bucket" in my

    basket And I have 2 "Large Spades" in my basket When I add 1 "Red Bucket" in my basket Then I should see 2 "Red Buckets" in my basket And I should see 2 "Large Spades" in my basket
  24. @mheap #phpkonf $username = $_GET['username'] ?? error('username is required'); $key

    = $_GET['key'] ?? error('key is required'); $validUsers = [ "michael" => "kangar00s", "oscar" => "phparch17" ]; $validKey = $validUsers[strtolower($username)] ?? error('could not find user', 404); if ($key !== $validKey){ error('invalid apikey'); } output([ 'success' => 'true', 'user' => ['name' => $username] ]);
  25. @mheap #phpkonf $username = $_GET['username'] ?? error('username is required'); $key

    = $_GET['key'] ?? error('key is required'); $validUsers = [ "michael" => "kangar00s", "oscar" => "phparch17" ]; $validKey = $validUsers[strtolower($username)] ?? error('could not find user', 404); if ($key !== $validKey){ error('invalid apikey'); } output([ 'success' => 'true', 'user' => ['name' => $username] ]);
  26. @mheap #phpkonf $username = $_GET['username'] ?? error('username is required'); $key

    = $_GET['key'] ?? error('key is required'); $validUsers = [ "michael" => "kangar00s", "oscar" => "phparch17" ]; $validKey = $validUsers[strtolower($username)] ?? error('could not find user', 404); if ($key !== $validKey){ error('invalid apikey'); } output([ 'success' => 'true', 'user' => ['name' => $username] ]);
  27. @mheap #phpkonf error('username is required'); error('key is required'); error('could not

    find user', 404); error('invalid apikey'); output([ 'success' => 'true', 'user' => ['name' => $username] ]); 1. Missing username 2. Missing key 3. Invalid username 4. Invalid key 5. Successful authentication
  28. @mheap #phpkonf Scenario: No Username When I make a "GET"

    request to "/" Then the response status code should be "400" And the "error" property equals "username is required"
  29. @mheap #phpkonf Scenario: No API Key When I make a

    "GET" request to "/?username=foo" Then the response status code should be "400" And the "error" property equals "key is required”
  30. @mheap #phpkonf Scenario: Invalid Username When I make a "GET"

    request to "/?username=bananas&key=foo" Then the response status code should be "404" And the "error" property equals "could not find user” Scenario: Invalid API Key When I make a "GET" request to "/?username=michael&key=foo" Then the response status code should be "400" And the "error" property equals "invalid apikey"
  31. @mheap #phpkonf Scenario: Successful login When I make a "GET"

    request to "/?username=michael&key=kangar00s" Then the response status code should be "200" And the "success" property equals "true" And the "user.name" property equals "michael”
  32. @mheap #phpkonf Scenario Outline: Successful login When I make a

    "GET" request to "/?username=<name>&key=<key>" Then the response status code should be "200" And the "success" property equals "true" And the "user.name" property equals "<name>" Examples: | name | key | | michael | kangar00s | | oscar | phparch17 |
  33. @mheap #phpkonf Given that header property "X-CustomAuthKey" is "Secret123" And

    that header property "Accept" is "application/json+vnd.v2" And that the request body is valid JSON ''' { "alpha":"beta", "count":3, "collection":["a","b","c"] } ''' When I make a "POST" request to "/account/balance" Then the response status code should be "200" And the "X-Remaining" header property equals "18" And the "balance.usd" property equals"1832.54" And the "balance.gbp" property equals "13.99" And the "balance.eur" property equals "19422.18"
  34. @mheap #phpkonf When I make a "POST" request to "/account/balance"

    Then the response status code should be “200" And the "X-Remaining" header property equals "18" And the response body contains the JSON data ''' { "in_credit": true, "balance": { "usd": 1832.54, "gbp": 13.99, "eur": 19422.18 } } '''
  35. @mheap #phpkonf When I make a "POST" request to "/account/balance"

    Then the response status code should be "200" And the "X-Remaining" header property equals "18" And the value of the "balance.usd" property matches the pattern “/^\d{1,3}\. \d{2}$/" And the value of the “last_updated” property matches the pattern "/^[0-9]{4} [\-][0-9]{2}[\-][0-9]{2} [0-9]{2}[:][0-9]{2}[:][0-9]{2}$/"
  36. @mheap #phpkonf # behat.yml default: extensions: DataSift\BehatExtension: base_url: "http: //localhost:8080/"

    suites: default: contexts: - 'DataSift\BehatExtension\Context\RestContext'
  37. @mheap #phpkonf function get_balance($username) { return $client ->get( “http: //localhost:88/billing/check_balance/".$username

    ) ->getBody(); } $balance = get_balance($username); if ($username != 'michael') { error('wrong username'); } if (!$balance['has_credit']) { error('no credit remaining'); } success(['data' => 'valid account'])
  38. @mheap #phpkonf Given Mountebank is running And a mock exists

    at "/billing/check_balance" it should return "200" with the body: ''' { "has_credit": false, "credit_limit": 0 } ''' And the mocks are created When I make a "GET" request to "/auth?username=michael" Then the response status code should be "403" And the response is JSON And the response body JSON equals ''' { "error": "no credit remaining" } '''
  39. @mheap #phpkonf Given Mountebank is running And a mock exists

    at "/billing/check_balance" it should return "200" with the body: ''' { "has_credit": true, "credit_limit": 10000 } ''' And the mocks are created When I make a "GET" request to "/auth?username=michael" Then the response status code should be "200" And the response is JSON And the response body JSON equals ''' { "data": "valid account" } '''
  40. @mheap #phpkonf $ composer require --dev behat/mink-extension $ composer require

    --dev behat/mink-selenium2-driver $ java -Dwebdriver.chrome.driver=chromedriver -jar selenium- server-standalone-3.7.1.jar
  41. @mheap #phpkonf # behat.yml default: extensions: Behat\MinkExtension: base_url: 'https: //wikipedia.org'

    sessions: default: selenium2: browser: "chrome" suites: my_suite: contexts: - \Behat\MinkExtension\Context\MinkContext
  42. @mheap #phpkonf 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"
  43. @mheap #phpkonf 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"
  44. @mheap #phpkonf Scenario: Searching for a page that does exist

    Given I am on "/wiki/Main_Page" When I search for "Behavior Driven Development" Then I should see "agile software development"
  45. @mheap #phpkonf Scenario: Searching for a page that does exist

    Given I am on "/wiki/Main_Page" When I search for "Behavior Driven Development" Then I should see "agile software development" 1 scenario (1 undefined) 3 steps (1 passed, 1 undefined, 1 skipped) 0m3.63s (10.62Mb) >> my_suite suite has undefined steps. Please choose the context to generate snippets: [0] None [1] FeatureContext [2] Behat\MinkExtension\Context\MinkContext
  46. @mheap #phpkonf [0] None [1] FeatureContext [2] Behat\MinkExtension\Context\MinkContext > 1

    --- FeatureContext has missing steps. Define them with these snippets: /** * @When I search for :arg1 */ public function iSearchFor($arg1) { throw new PendingException(); }
  47. @mheap #phpkonf [0] None [1] FeatureContext [2] Behat\MinkExtension\Context\MinkContext > 1

    u features/bootstrap/FeatureContext.php - `I search for "Behavior Driven Development"` definition added
  48. @mheap #phpkonf Feature: Test Scenario: Searching for a page that

    does exist Given I am on "/wiki/Main_Page" When I search for "Behavior Driven Development" TODO: write pending definition Then I should see "agile software development" 1 scenario (1 pending) 3 steps (1 passed, 1 pending, 1 skipped) 0m2.23s (10.61Mb)
  49. @mheap #phpkonf /** @BeforeScenario */ public function gatherContexts( Behat\Behat\Hook\Scope\BeforeScenarioScope $scope

    ) { $environment = $scope ->getEnvironment(); $this ->minkContext = $environment ->getContext( 'Behat\MinkExtension\Context\MinkContext' ); }
  50. @mheap #phpkonf /** * Fills in form field with specified

    id|name|label|value * Example: When I fill in "username" with: "bwayne" * Example: And I fill in "bwayne" for "username" * * @When /^( ?:|I )fill in "(?P<field>( ?:[^"]|\ ")*)" with "(?P<value>( ?:[^"]|\ ")*)"$/ * @When /^( ?:|I )fill in "(?P<field>( ?:[^"]|\ ")*)" with:$/ * @When /^( ?:|I )fill in "(?P<value>( ?:[^"]|\ ")*)" for "(?P<field>( ?:[^"]|\ ")*)"$/ */ public function fillField($field, $value) { $field = $this ->fixStepArgument($field); $value = $this ->fixStepArgument($value); $this ->getSession() ->getPage() ->fillField($field, $value); }
  51. @mheap #phpkonf /** * Presses button with specified id|name|title|alt|value *

    Example: When I press "Log In" * Example: And I press "Log In" * @When /^( ?:|I )press "(?P<button>( ?:[^"]|\ ")*)"$/ */ public function pressButton($button) { $button = $this ->fixStepArgument($button); $this ->getSession() ->getPage() ->pressButton($button); }
  52. @mheap #phpkonf /** * @When I search for :term */

    public function iSearchFor($term) { $this ->minkContext ->fillField('search', $term); $this ->minkContext ->pressButton('searchButton'); }
  53. @mheap #phpkonf $ ./vendor/bin/behat Feature: Test Scenario: Searching for a

    page that does exist Given I am on "/wiki/Main_Page" When I search for "Behavior Driven Development" Then I should see "agile software development" 1 scenario (1 passed) 3 steps (3 passed)
  54. @mheap #phpkonf Scenario: Successful login When I make a "GET"

    request to "/?username=michael&key=kangar00s" Then the response status code should be "200" And the "success" property equals "true" And the "user.name" property equals "michael”
  55. @mheap #phpkonf Scenario: Successful login When I log in as

    "Michael" with the password "kangar00s" Then the response status code should be "200" And the "success" property equals "true" And the "user.name" property equals "michael”
  56. @mheap #phpkonf public function toJson(){ return json_encode(["id" => $this ->id]);

    } —— public function test_json_formatting_is_correct() { $order = new Order(1); $this ->assertMatchesSnapshot($order ->toJson()); }
  57. @mheap #phpkonf $ ./vendor/bin/phpunit There was 1 incomplete test: 1)

    ExampleTest ::test_json_formatting_is_correct Snapshot created for ExampleTest __test_json_formatting_is_correct
  58. @mheap #phpkonf $ ./vendor/bin/phpunit 1) ExampleTest ::test_json_formatting_is_correct Failed asserting that

    two strings are equal. --- Expected +++ Actual @@ @@ -'{"id": 1}' +'{"id": "Q1"}' FAILURES! Tests: 1, Assertions: 1, Failures: 1.