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

Behat for Characterization Tests

Michael Heap
November 15, 2017

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 behavior, and we need a way to ensure 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 behavior too. We’ve used these tools to gain confidence to refactor 5+-year-old apps by capturing the existing behavior before making changes. I want to share the secrets we learned with you.

Michael Heap

November 15, 2017
Tweet

More Decks by Michael Heap

Other Decks in Programming

Transcript

  1. @mheap
    #phpworld
    for
    characterization tests

    View Slide

  2. @mheap
    #phpworld
    Imagine this…

    View Slide

  3. @mheap
    #phpworld
    $$$

    View Slide

  4. @mheap
    #phpworld
    $$$
    $$$
    $$$

    View Slide

  5. @mheap
    #phpworld
    $$$ Billing Rating
    Auth Report
    Masking Encoding
    $$$
    $$$

    View Slide

  6. @mheap
    #phpworld
    $$$ Billing Rating
    Auth Report
    Masking Encoding
    $$$
    $$$

    View Slide

  7. @mheap
    #phpworld
    $$$
    $$$
    $$$
    Billing Rating
    Auth Report
    Masking Encoding
    public function provides()
    {
    return [Client ::class];
    }

    View Slide

  8. @mheap
    #phpworld

    View Slide

  9. @mheap
    #phpworld
    $ ls -l test
    total 0

    View Slide

  10. @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.

    View Slide

  11. @mheap
    #phpworld
    Imagine

    View Slide

  12. @mheap
    #phpworld
    Hello, I’m Michael

    View Slide

  13. @mheap
    #phpworld
    @mheap

    View Slide

  14. @mheap
    #phpworld

    View Slide

  15. View Slide

  16. @mheap
    #phpworld
    Today

    View Slide

  17. @mheap
    #phpworld
    What is legacy code?

    View Slide

  18. @mheap
    Michael Feathers - Working Effectively with Legacy Code
    “code without tests”
    Legacy Code is

    View Slide

  19. @mheap
    Legacy Code is
    “valuable code that
    we feel afraid to
    change”
    JB Rainsberger - Surviving Legacy Code with Golden Master and Sampling

    View Slide

  20. @mheap
    #phpworld
    What if I make a change
    and break something?

    View Slide

  21. @mheap
    #phpworld
    What if I make a change
    and change something?

    View Slide

  22. @mheap
    #phpworld
    Intended
    or
    Unintended

    View Slide

  23. @mheap
    #phpworld
    Intended
    or
    Unintended

    View Slide

  24. @mheap
    #phpworld
    Intended
    or
    Unintended

    View Slide

  25. @mheap
    #phpworld
    But there aren’t
    any tests!

    View Slide

  26. @mheap
    #phpworld
    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;
    }
    }
    }

    View Slide

  27. @mheap
    #phpworld
    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;
    }
    }
    }

    View Slide

  28. @mheap
    #phpworld
    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;
    }
    }
    }

    View Slide

  29. @mheap
    #phpworld
    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;
    }
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  33. @mheap
    #phpworld
    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.

    View Slide

  34. @mheap
    #phpworld
    use PHPUnit\Framework\TestCase;
    class SalesUtilTest extends TestCase
    {
    public function testSalesLTE1000Pay20PercentCommission()
    {
    $this ->assertEquals(200, SalesUtil ::calculate(1000));
    }
    }

    View Slide

  35. @mheap
    #phpworld
    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;
    }
    }
    }

    View Slide

  36. @mheap
    #phpworld
    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.

    View Slide

  37. @mheap
    #phpworld
    public function test2000Gets500Commission()
    {
    $this ->assertEquals(500, SalesUtil ::calculate(2000));
    }
    public function testX()
    {
    $this ->assertEquals(null, SalesUtil ::calculate(1600));
    }

    View Slide

  38. @mheap
    #phpworld
    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.

    View Slide

  39. @mheap
    #phpworld
    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;
    }
    }
    }

    View Slide

  40. @mheap
    #phpworld
    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

    View Slide

  41. @mheap
    #phpworld
    public function testX()
    {
    $this ->assertEquals(null, SalesUtil ::calculate(-123));
    }

    View Slide

  42. @mheap
    #phpworld
    public function testNegativeSalesGivesCommissionIs()
    {
    $this ->assertEquals(-24.6, SalesUtil ::calculate(-123));
    }

    View Slide

  43. @mheap
    #phpworld
    Do not change the
    code

    View Slide

  44. @mheap
    #phpworld
    Do not change the
    code

    View Slide

  45. @mheap
    #phpworld
    Do NOT change the
    code

    View Slide

  46. @mheap
    https://en.wikipedia.org/wiki/Equivalence_partitioning
    What’s the minimum
    number of tests?

    View Slide

  47. @mheap
    #phpworld
    We’re half way!
    (and we’ve barely mentioned Behat)

    View Slide

  48. @mheap
    #phpworld
    Choosing your
    pinch points

    View Slide

  49. @mheap
    #phpworld
    Choose HTTP

    View Slide

  50. @mheap
    #phpworld
    Choose Behat

    View Slide

  51. @mheap
    #phpworld
    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

    View Slide

  52. @mheap
    #phpworld
    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

    View Slide

  53. @mheap
    #phpworld
    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

    View Slide

  54. @mheap
    #phpworld
    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

    View Slide

  55. @mheap
    #phpworld
    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

    View Slide

  56. @mheap
    #phpworld
    Be pragmatic

    View Slide

  57. @mheap
    #phpworld
    composer require behat/behat --dev
    vendor/bin/behat --init

    View Slide

  58. @mheap
    #phpworld
    Testing a HTTP API

    View Slide

  59. @mheap
    #phpworld
    $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]
    ]);

    View Slide

  60. @mheap
    #phpworld
    $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]
    ]);

    View Slide

  61. @mheap
    #phpworld
    $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]
    ]);

    View Slide

  62. @mheap
    #phpworld
    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

    View Slide

  63. @mheap
    #phpworld
    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"

    View Slide

  64. @mheap
    #phpworld
    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”

    View Slide

  65. @mheap
    #phpworld
    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"

    View Slide

  66. @mheap
    #phpworld
    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”

    View Slide

  67. @mheap
    #phpworld
    Scenario Outline: Successful login
    When I make a "GET" request to "/?username=&key="
    Then the response status code should be "200"
    And the "success" property equals "true"
    And the "user.name" property equals ""
    Examples:
    | name | key |
    | michael | kangar00s |
    | oscar | phparch16 |

    View Slide

  68. @mheap
    #phpworld
    More complex tests

    View Slide

  69. @mheap
    #phpworld
    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"

    View Slide

  70. @mheap
    #phpworld
    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
    }
    }
    '''

    View Slide

  71. @mheap
    #phpworld
    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}$/"

    View Slide

  72. @mheap
    #phpworld
    composer require datasift/testrest-extension

    View Slide

  73. @mheap
    #phpworld
    # behat.yml
    default:
    extensions:
    DataSift\BehatExtension:
    base_url: "http: //localhost:8080/"
    suites:
    default:
    contexts:
    - 'DataSift\BehatExtension\Context\RestContext'

    View Slide

  74. @mheap
    #phpworld
    HTTP Mocks

    View Slide

  75. @mheap
    #phpworld
    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'])

    View Slide

  76. @mheap
    #phpworld
    Mountebank

    View Slide

  77. @mheap
    #phpworld
    npm install -g mountebank

    View Slide

  78. @mheap
    #phpworld
    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" }
    '''

    View Slide

  79. @mheap
    #phpworld
    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" }
    '''

    View Slide

  80. @mheap
    #phpworld
    Automating a browser

    View Slide

  81. @mheap
    #phpworld
    $ 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

    View Slide

  82. @mheap
    #phpworld
    # behat.yml
    default:
    extensions:
    Behat\MinkExtension:
    base_url: 'https: //wikipedia.org'
    sessions:
    default:
    selenium2:
    browser: "chrome"
    suites:
    my_suite:
    contexts:
    - \Behat\MinkExtension\Context\MinkContext

    View Slide

  83. @mheap
    #phpworld
    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"

    View Slide

  84. @mheap
    #phpworld
    Custom Steps

    View Slide

  85. @mheap
    #phpworld
    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"

    View Slide

  86. @mheap
    #phpworld
    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"

    View Slide

  87. @mheap
    #phpworld
    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

    View Slide

  88. @mheap
    #phpworld
    [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();
    }

    View Slide

  89. @mheap
    #phpworld
    ./vendor/bin/behat --append-snippets

    View Slide

  90. @mheap
    #phpworld
    [0] None
    [1] FeatureContext
    [2] Behat\MinkExtension\Context\MinkContext
    > 1
    u features/bootstrap/FeatureContext.php - `I search for "Behavior Driven
    Development"` definition added

    View Slide

  91. @mheap
    #phpworld
    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)

    View Slide

  92. @mheap
    #phpworld
    /** @BeforeScenario */
    public function gatherContexts(
    Behat\Behat\Hook\Scope\BeforeScenarioScope $scope
    ) {
    $environment = $scope ->getEnvironment();
    $this ->minkContext = $environment ->getContext(
    'Behat\MinkExtension\Context\MinkContext'
    );
    }

    View Slide

  93. @mheap
    #phpworld
    /**
    * 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( ?:[^"]| \\")*)" with "(?P( ?:[^"]|
    \\")*)"$/
    * @When /^( ?:|I )fill in "(?P( ?:[^"]| \\")*)" with:$/
    * @When /^( ?:|I )fill in "(?P( ?:[^"]| \\")*)" for "(?P( ?:[^"]|
    \\")*)"$/
    */
    public function fillField($field, $value)
    {
    $field = $this ->fixStepArgument($field);
    $value = $this ->fixStepArgument($value);
    $this ->getSession() ->getPage() ->fillField($field, $value);
    }

    View Slide

  94. @mheap
    #phpworld
    /**
    * 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( ?:[^"]| \\")*)"$/
    */
    public function pressButton($button)
    {
    $button = $this ->fixStepArgument($button);
    $this ->getSession() ->getPage() ->pressButton($button);
    }

    View Slide

  95. @mheap
    #phpworld
    /**
    * @When I search for :term
    */
    public function iSearchFor($term)
    {
    $this ->minkContext ->fillField('search', $term);
    $this ->minkContext ->pressButton('searchButton');
    }

    View Slide

  96. @mheap
    #phpworld
    $ ./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)

    View Slide

  97. @mheap
    #phpworld
    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”

    View Slide

  98. @mheap
    #phpworld
    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”

    View Slide

  99. @mheap
    #phpworld
    Snapshot testing

    View Slide

  100. @mheap
    #phpworld
    public function toJson(){
    return json_encode(["id" => $this ->id]);
    }
    ——
    public function test_json_formatting_is_correct() {
    $order = new Order(1);
    $this ->assertMatchesSnapshot($order ->toJson());
    }

    View Slide

  101. @mheap
    #phpworld
    $ ./vendor/bin/phpunit
    There was 1 incomplete test:
    1) ExampleTest ::test_json_formatting_is_correct
    Snapshot created for ExampleTest __test_json_formatting_is_correct

    View Slide

  102. @mheap
    #phpworld
    $ ./vendor/bin/phpunit
    OK (1 test, 1 assertion)

    View Slide

  103. @mheap
    #phpworld
    public function toJson(){
    return json_encode(["id" => 'Q'.$this ->id]);
    }

    View Slide

  104. @mheap
    #phpworld
    $ ./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.

    View Slide

  105. @mheap
    #phpworld
    https: //github.com/spatie/phpunit-snapshot-assertions

    View Slide

  106. @mheap
    #phpworld
    Conclusion

    View Slide

  107. @mheap
    #phpworld
    We don’t have
    time for this

    View Slide

  108. @mheap
    #phpworld
    Where should we
    start?

    View Slide

  109. @mheap
    #phpworld
    Start refactoring

    View Slide

  110. @mheap
    #phpworld
    Characterisation tests
    aren’t perfect

    View Slide

  111. @mheap
    #phpworld
    The 4 stages of
    characterisation tests

    View Slide

  112. @mheap
    #phpworld
    Examine
    your feature

    View Slide

  113. @mheap
    #phpworld
    Characterize
    your feature

    View Slide

  114. @mheap
    #phpworld
    Refactor
    your feature

    View Slide

  115. @mheap
    #phpworld
    Delete
    your tests

    View Slide

  116. @mheap
    #phpworld
    for
    characterization tests

    View Slide

  117. @mheap
    #phpworld
    I’m Michael

    View Slide

  118. @mheap
    #phpworld
    @mheap

    View Slide

  119. @mheap
    #phpworld
    https://joind.in/21790

    View Slide