Slide 1

Slide 1 text

@yot88 Approval testing kata (gilded rose)

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

@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

Slide 7

Slide 7 text

@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

Slide 8

Slide 8 text

@yot88 How to ? Tooling

Slide 9

Slide 9 text

@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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

@yot88

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

@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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

@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); }

Slide 17

Slide 17 text

@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]); }

Slide 18

Slide 18 text

@yot88 Run the test Now it should fail

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

@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

Slide 21

Slide 21 text

@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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

@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

Slide 26

Slide 26 text

@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

Slide 27

Slide 27 text

@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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

@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 ?

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

@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

Slide 32

Slide 32 text

@yot88 Let’s kill mutants

Slide 33

Slide 33 text

@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} ); }

Slide 34

Slide 34 text

@yot88 We are now ready to implement

Slide 35

Slide 35 text

@yot88 conclusion Liked Learned Lacked Longed for

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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/