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 Objectives • Learn a practice that will help you

    be quickly productive in an unfamiliar environment • Use Approval Testing to deal with legacy code
  2. @yot88 Before we code On a sticky note, write a

    question you want answered about Approval Testing
  3. @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.
  4. @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.
  5. @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
  6. @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
  7. @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
  8. @yot88 individually • What do you think about this code

    ? • If you must add a new type of items what would be your strategy ?
  9. 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
  10. @yot88 How many tests would you write before being confident

    enough to refactor the code ? Which ones ? Testing
  11. @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
  12. @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); }
  13. @yot88 Let's use an approval test now Add ApprovalTests dependency

    in your pom.xml <dependency> <groupId>com.approvaltests</groupId> <artifactId>approvaltests</artifactId> <version>9.1.0</version> <scope>test</scope> </dependency> 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]); }
  14. @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.
  15. @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
  16. @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
  17. @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.
  18. @yot88 Use CombinationApprovals CombinationApprovals allow to combine a lot of

    inputs in the same ApprovalTests. Function as 1st arg
  19. @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
  20. @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
  21. @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
  22. @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 ?
  23. @yot88 Are we confident enough ? Code coverage is a

    quantitative metric. To have a quality one we can use Mutation testing.
  24. @yot88 Are we confident enough ? Let's use pitest to

    discover if we can improve our tests : <plugin> <groupId>org.pitest</groupId> <artifactId>pitest-maven</artifactId> <version>1.5.0</version> <dependencies> <dependency> <groupId>org.pitest</groupId> <artifactId>pitest-junit5-plugin</artifactId> <version>0.8</version> </dependency> </dependencies> </plugin>
  25. @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} ); }
  26. 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/