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

Approval testing kata - Gilded Rose

Yoan
August 05, 2020

Approval testing kata - Gilded Rose

Learn how Approval testing can help you when dealing with legacy code

Yoan

August 05, 2020
Tweet

More Decks by Yoan

Other Decks in Programming

Transcript

  1. @yot88
    Approval testing kata
    (gilded rose)

    View Slide

  2. @yot88
    Objectives
    • Learn a practice that will help you be quickly productive in an
    unfamiliar environment
    • Use Approval Testing to deal with legacy code

    View Slide

  3. @yot88
    Before we code
    On a sticky note, write a question you want answered about
    Approval Testing

    View Slide

  4. @yot88
    What is Approval Testing ?
    Approval Tests are also called Snapshot Tests or Golden Master
    Approval tests work by creating an output that needs human
    approval / verification.

    View Slide

  5. @yot88
    What is Approval Testing ?
    Once the initial output has been defined and “APPROVED” then as long as the test provides
    consistent output then the test will continue to pass.
    Compare your implementation/actual program against approved outputs
    Once the test provides output that is different to the approved output the test will fail.

    View Slide

  6. @yot88
    The difference with Assert-based tests
    • Unit testing asserts can be difficult to use
    • Approval tests simplify this by taking a snapshot of the results
    confirming that they have not changed
    assertEquals(5, person.getAge())
    ...
    As many asserts than you want to check fields
    Approvals.verify(person)
    Always only 1 single line

    View Slide

  7. @yot88
    Main characteristics
    • human inspects and approves some actual program output when creating a test case.
    • Design a Printer to display complex objects, instead of many assertions.
    • If actual program output is not yet available, the approved value may be a manual sketch of the expected output
    (useful when you do TDD).
    • Approved values are stored separately from the source code for the test case

    View Slide

  8. @yot88
    How to ?
    Tooling

    View Slide

  9. @yot88
    Let’s see some code
    • Clone the repository here
    Make sure you can build it
    • Read the specifications in the Readme
    • Look at the code

    View Slide

  10. @yot88
    individually
    • What do you think about this code ?
    • If you must add a new type of items what would be your strategy ?

    View Slide

  11. @yot88

    View Slide

  12. public void updateQuality() {
    for (int i = 0; i < items.length; i++) {
    if (!items[i].name.equals("Aged Brie")
    && !items[i].name.equals("Backstage passes to a TAFKAL80ETC concert")) {
    if (items[i].quality > 0) {
    if (!items[i].name.equals("Sulfuras, Hand of Ragnaros")) {
    items[i].quality = items[i].quality - 1;
    }
    }
    } else {
    if (items[i].quality < 50) {
    items[i].quality = items[i].quality + 1;
    if (items[i].name.equals("Backstage passes to a TAFKAL80ETC concert")) {
    if (items[i].sellIn < 11) {
    if (items[i].quality < 50) {
    items[i].quality = items[i].quality + 1;
    }
    }
    if (items[i].sellIn < 6) {
    if (items[i].quality < 50) {
    items[i].quality = items[i].quality + 1;
    }
    }
    }
    }
    }
    if (!items[i].name.equals("Sulfuras, Hand of Ragnaros")) {
    items[i].sellIn = items[i].sellIn - 1;
    }
    if (items[i].sellIn < 0) {
    if (!items[i].name.equals("Aged Brie")) {
    if (!items[i].name.equals("Backstage passes to a TAFKAL80ETC concert")) {
    if (items[i].quality > 0) {
    if (!items[i].name.equals("Sulfuras, Hand of Ragnaros")) {
    items[i].quality = items[i].quality - 1;
    }
    }
    } else {
    items[i].quality = items[i].quality - items[i].quality;
    }
    } else {
    if (items[i].quality < 50) {
    items[i].quality = items[i].quality + 1;
    }
    }
    }
    }
    }
    https://plugins.jetbrains.com/plugin/12159-codemetrics

    View Slide

  13. @yot88
    How many tests would you write before being confident
    enough to refactor the code ?
    Which ones ?
    Testing

    View Slide

  14. @yot88
    We have recently signed a supplier of conjured items.
    This requires an update to our system:
    "Conjured" items degrade in Quality twice as fast as normal items
    Exercise

    View Slide

  15. @yot88
    Baby steps
    Let’s do it by using baby steps

    View Slide

  16. @yot88
    Write a first test
    Based on the specifications write a first test by using junit 5 (dependency already in your pom)
    @Test
    public void updateQuality() {
    var items = new Item[]{new Item("a common item", 0, 0)};
    var gildedRose = new GildedRose(items);
    gildedRose.updateQuality();
    assertEquals("a common item", gildedRose.items[0].name);
    assertEquals(-1, gildedRose.items[0].sellIn);
    assertEquals(0, gildedRose.items[0].quality);
    }

    View Slide

  17. @yot88
    Let's use an approval test now
    Add ApprovalTests dependency in your pom.xml

    com.approvaltests
    approvaltests
    9.1.0
    test

    Refactor the existing test using ApprovalTests
    @Test
    public void updateQuality() {
    var items = new Item[]{new Item("a common item", 0, 0)};
    var gildedRose = new GildedRose(items);
    gildedRose.updateQuality();
    Approvals.verify(gildedRose.items[0]);
    }

    View Slide

  18. @yot88
    Run the test
    Now it should fail

    View Slide

  19. @yot88
    Run the test
    ApprovalTests library compares 2 files :
    • GildedRoseTests.updateQuality.received.txt that has been generated based on what is inside the verify method call
    • GildedRoseTests.updateQuality.approved.txt a content that has already been approved
    The actual implementation is functionally good.
    So we must approve what is currently generated / calculated by the system.

    View Slide

  20. @yot88
    Approve the content of the file
    mv GildedRoseTests.updateQuality.received.txt
    GildedRoseTests.updateQuality.approved.txt
    If you run the test again, it should be green now
    Commit this
    Never store the received

    View Slide

  21. @yot88
    What about coverage ?
    Before making a refactoring, you must be confident about the tests covering the code you want to refactor.
    To do so you can run your test with Coverage

    View Slide

  22. @yot88
    If you use IntelliJ IDEA
    Enable tracing option Run your test with coverage again :

    View Slide

  23. @yot88
    Use Code Coverage to increase our confidence
    What I recommend when you use Code Coverage or design tests is to always have your Subject Under Test in front
    of you : split your screen vertically.

    View Slide

  24. @yot88
    Use CombinationApprovals
    CombinationApprovals allow to combine a lot of inputs in the same ApprovalTests.
    Function as 1st arg

    View Slide

  25. @yot88
    Refactor the test to use CombinationApprovals
    @Test
    public void updateQuality() {
    var name = "a common item";
    var sellIn = 0;
    var quality = 0;
    CombinationApprovals.verifyAllCombinations(
    this::callUpdateQuality,
    new String[]{name},
    new Integer[]{sellIn},
    new Integer[]{quality}
    );
    }
    private String callUpdateQuality(String name, int sellIn, int quality) {
    var items = new Item[]{new Item(name, sellIn, quality)};
    var gildedRose = new GildedRose(items);
    gildedRose.updateQuality();
    return gildedRose.items[0].toString();
    }
    Run the test again
    when you use CombinationApprovals it adds a description of the
    combination for each input :
    [a common item, 0, 0] => a common item, -1, 0

    View Slide

  26. @yot88
    Cover new lines of codes
    1. Go on a red or orange line
    2. Add an input in the
    CombinationApprovals
    3. Run the test with
    coverage
    (check that you improved the
    coverage)
    4. Approve the result
    (with your terminal)
    Repeat until you have 100% coverage and are
    very confident
    By discovering them with the Code Coverage tool

    View Slide

  27. @yot88
    At the end
    @Test
    public void updateQuality() {
    CombinationApprovals.verifyAllCombinations(
    this::callUpdateQuality,
    new String[]{"a common item",
    "Aged Brie",
    "Backstage passes to a TAFKAL80ETC concert",
    "Sulfuras, Hand of Ragnaros"},
    new Integer[]{-1, 0, 11},
    new Integer[]{0, 1, 49, 50}
    );
    }
    [a common item, -1, 0] => a common item, -2, 0
    [a common item, -1, 1] => a common item, -2, 0
    [a common item, -1, 49] => a common item, -2, 47
    [a common item, -1, 50] => a common item, -2, 48
    [a common item, 0, 0] => a common item, -1, 0
    [a common item, 0, 1] => a common item, -1, 0
    [a common item, 0, 49] => a common item, -1, 47
    [a common item, 0, 50] => a common item, -1, 48
    [a common item, 11, 0] => a common item, 10, 0
    [a common item, 11, 1] => a common item, 10, 0
    [a common item, 11, 49] => a common item, 10, 48
    [a common item, 11, 50] => a common item, 10, 49
    [Aged Brie, -1, 0] => Aged Brie, -2, 2
    [Aged Brie, -1, 1] => Aged Brie, -2, 3
    [Aged Brie, -1, 49] => Aged Brie, -2, 50
    [Aged Brie, -1, 50] => Aged Brie, -2, 50
    [Aged Brie, 0, 0] => Aged Brie, -1, 2
    [Aged Brie, 0, 1] => Aged Brie, -1, 3
    [Aged Brie, 0, 49] => Aged Brie, -1, 50
    [Aged Brie, 0, 50] => Aged Brie, -1, 50
    [Aged Brie, 11, 0] => Aged Brie, 10, 1
    [Aged Brie, 11, 1] => Aged Brie, 10, 2
    [Aged Brie, 11, 49] => Aged Brie, 10, 50
    [Aged Brie, 11, 50] => Aged Brie, 10, 50
    [Backstage passes to a TAFKAL80ETC concert, -1, 0] => Backstage passes to a TAFKAL80ETC concert, -2, 0
    [Backstage passes to a TAFKAL80ETC concert, -1, 1] => Backstage passes to a TAFKAL80ETC concert, -2, 0
    [Backstage passes to a TAFKAL80ETC concert, -1, 49] => Backstage passes to a TAFKAL80ETC concert, -2, 0
    [Backstage passes to a TAFKAL80ETC concert, -1, 50] => Backstage passes to a TAFKAL80ETC concert, -2, 0
    [Backstage passes to a TAFKAL80ETC concert, 0, 0] => Backstage passes to a TAFKAL80ETC concert, -1, 0
    [Backstage passes to a TAFKAL80ETC concert, 0, 1] => Backstage passes to a TAFKAL80ETC concert, -1, 0
    [Backstage passes to a TAFKAL80ETC concert, 0, 49] => Backstage passes to a TAFKAL80ETC concert, -1, 0
    [Backstage passes to a TAFKAL80ETC concert, 0, 50] => Backstage passes to a TAFKAL80ETC concert, -1, 0
    [Backstage passes to a TAFKAL80ETC concert, 11, 0] => Backstage passes to a TAFKAL80ETC concert, 10, 1
    [Backstage passes to a TAFKAL80ETC concert, 11, 1] => Backstage passes to a TAFKAL80ETC concert, 10, 2
    [Backstage passes to a TAFKAL80ETC concert, 11, 49] => Backstage passes to a TAFKAL80ETC concert, 10, 50
    [Backstage passes to a TAFKAL80ETC concert, 11, 50] => Backstage passes to a TAFKAL80ETC concert, 10, 50
    [Sulfuras, Hand of Ragnaros, -1, 0] => Sulfuras, Hand of Ragnaros, -1, 0
    [Sulfuras, Hand of Ragnaros, -1, 1] => Sulfuras, Hand of Ragnaros, -1, 1
    [Sulfuras, Hand of Ragnaros, -1, 49] => Sulfuras, Hand of Ragnaros, -1, 49
    [Sulfuras, Hand of Ragnaros, -1, 50] => Sulfuras, Hand of Ragnaros, -1, 50
    [Sulfuras, Hand of Ragnaros, 0, 0] => Sulfuras, Hand of Ragnaros, 0, 0
    [Sulfuras, Hand of Ragnaros, 0, 1] => Sulfuras, Hand of Ragnaros, 0, 1
    [Sulfuras, Hand of Ragnaros, 0, 49] => Sulfuras, Hand of Ragnaros, 0, 49
    [Sulfuras, Hand of Ragnaros, 0, 50] => Sulfuras, Hand of Ragnaros, 0, 50
    [Sulfuras, Hand of Ragnaros, 11, 0] => Sulfuras, Hand of Ragnaros, 11, 0
    [Sulfuras, Hand of Ragnaros, 11, 1] => Sulfuras, Hand of Ragnaros, 11, 1
    [Sulfuras, Hand of Ragnaros, 11, 49] => Sulfuras, Hand of Ragnaros, 11, 49
    [Sulfuras, Hand of Ragnaros, 11, 50] => Sulfuras, Hand of Ragnaros, 11, 50

    View Slide

  28. @yot88
    Let’s refactor now
    What’s the next step ?

    View Slide

  29. @yot88
    Let’s refactor now
    Always wonder :
    Am I confident enough ?
    Mutate the line 26 manually :
    • Simply replacing the integer 1 by another random integer
    • Run the test again, what happens ?

    View Slide

  30. @yot88
    Are we confident enough ?
    Code coverage is a quantitative metric.
    To have a quality one we can use Mutation testing.

    View Slide

  31. @yot88
    Are we confident enough ?
    Let's use pitest to discover if we can improve our tests :

    org.pitest
    pitest-maven
    1.5.0


    org.pitest
    pitest-junit5-plugin
    0.8



    View Slide

  32. @yot88
    Let’s kill mutants

    View Slide

  33. @yot88
    We end up with
    @Test
    public void updateQuality() {
    CombinationApprovals.verifyAllCombinations(
    this::callUpdateQuality,
    new String[]{"a common item",
    "Aged Brie",
    "Backstage passes to a TAFKAL80ETC concert",
    "Sulfuras, Hand of Ragnaros"},
    new Integer[]{-100, -1, 0, 2, 8, 11},
    new Integer[]{0, 1, 49, 50}
    );
    }

    View Slide

  34. @yot88
    We are now ready to implement

    View Slide

  35. @yot88
    conclusion
    Liked Learned
    Lacked Longed for

    View Slide

  36. @yot88
    resources
    https://emilybache.github.io/workshops/approvals/
    https://approvaltests.com/

    View Slide

  37. Who am I ?
    Technical Agile coach, Software craftsman
    I’m Yoan THIRION (freelance)
    • design software since more than 15 years
    • fundamental to succeed in that area : agility and technical excellence
    • help teams deliver well crafted software
    • implementation of agile and technical practices (eXtreme programming, Refactoring, DDD, Mob programming, …)
    Let’s connect
    My services
    https://www.yoan-thirion.com/

    View Slide