Skip to content

Commit ec9f56a

Browse files
committed
Proposed Solution Day 15.
1 parent edff26f commit ec9f56a

17 files changed

+360
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ A solution proposal will be published here every day during the `Advent Of Craft
7979
- [Day 12: Make your code open for extension.](solution/day12/docs/step-by-step.md)
8080
- [Day 13: Find a way to eliminate the irrelevant, and amplify the essentials of those tests.](solution/day13/docs/step-by-step.md)
8181
- [Day 14: Do not use exceptions anymore.](solution/day14/docs/step-by-step.md)
82-
82+
- [Day 15: Put a code under tests.](solution/day15/docs/step-by-step.md)
8383

8484
## Contributors
8585

solution/day15/docs/challenge-done.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
## Day 15: Put a code under tests.
2+
3+
Imagine we need to adapt the code below to support a new document template
4+
- We want to be sure to not introduce regression / have a safety net
5+
6+
### Assessing the right type of tests
7+
8+
- Let's add some tests
9+
- We have plenty of possible combinations
10+
- We could use `ParameterizedTests` to make those assertions
11+
12+
### Use Approval Testing instead
13+
14+
We can use `Approval Testing` to quickly put legacy code under tests.
15+
16+
It is also called : `Characterization Tests` OR `Golden Master`
17+
- Unit testing assertions can be difficult to use and long to write
18+
- Approval tests simplify this by taking a snapshot of the results / confirming that they have not changed at each run
19+
20+
Let's set that up:
21+
- We add [ApprovalTests](https://github.com/approvals/approvaltests.java) dependency in our pom
22+
- Add `.gitignore` file to exclude `*.received.*` from git
23+
- Instead, let's use the power of Approval !!!
24+
- We can generate combinations and have only 1 test (golden master) using `CombinationApprovals.verifyAllCombinations`
25+
26+
How it works:
27+
- On the first run, 2 files are created
28+
- `DocumentTests.combinationTests.received.txt`: the result of the call of the method under test
29+
- `DocumentTests.combinationTests.approved.txt`: the approved version of the result (approved manually)
30+
- The library simply compare the 2 text files, so it fails the first time you run it
31+
- It compares the actual result and an empty file
32+
33+
- We need to approve the `received` file to make the test passes
34+
- Meaning we create the `approved` one with the result of the current production code
35+
36+
### Refactoring time
37+
- We can even improve the test by making it totally dynamic
38+
- If we add a new Enum entry the test will fail
39+
- Forcing us to approve the new version of the test output
40+
41+
- In just a few minutes, we have successfully covered a cryptic code with robust tests
42+
43+
>**Tip of the day: Choosing the right type of test can help you gather better feedback on your code.**
44+
45+
### Share your experience
46+
47+
How does your code look like?
48+
49+
Please let everyone know in the discord.

solution/day15/docs/img/2-files.png

32.8 KB
Loading
Loading
36.8 KB
Loading

solution/day15/docs/img/fail.png

287 KB
Loading
429 KB
Loading

solution/day15/docs/img/first-run.png

286 KB
Loading
326 KB
Loading

solution/day15/docs/snippet.png

238 KB
Loading

solution/day15/docs/step-by-step.md

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
## Day 15: Put a code under tests.
2+
- Imagine we need to adapt the code below to support a new document template
3+
- We want to be sure to not introduce regression / have a safety net
4+
5+
```java
6+
public enum DocumentTemplateType {
7+
DRP("DEER", I),
8+
DPM("DEER", L),
9+
ATP("AUTP", I),
10+
ATM("AUTM", L),
11+
SPEC("SPEC", ALL),
12+
GLP("GLPP", I),
13+
GLM("GLPM", L);
14+
15+
private final String documentType;
16+
private final RecordType recordType;
17+
18+
DocumentTemplateType(String documentType, RecordType recordType) {
19+
this.documentType = documentType;
20+
this.recordType = recordType;
21+
}
22+
23+
public static DocumentTemplateType fromDocumentTypeAndRecordType(String documentType, String recordType) {
24+
for (DocumentTemplateType dtt : DocumentTemplateType.values()) {
25+
if (dtt.getDocumentType().equalsIgnoreCase(documentType)
26+
&& dtt.getRecordType().equals(RecordType.valueOf(recordType))) {
27+
return dtt;
28+
} else if (dtt.getDocumentType().equalsIgnoreCase(documentType)
29+
&& dtt.getRecordType().equals(ALL)) {
30+
return dtt;
31+
}
32+
}
33+
throw new IllegalArgumentException("Invalid Document template type or record type");
34+
}
35+
36+
private RecordType getRecordType() {
37+
return recordType;
38+
}
39+
40+
private String getDocumentType() {
41+
return documentType;
42+
}
43+
}
44+
```
45+
46+
- Let's add some tests
47+
- We have plenty of possible combinations
48+
- We could use `ParameterizedTests` to make those assertions
49+
50+
```java
51+
@Test
52+
void given_glpp_and_individual_prospect_should_return_glpp() {
53+
final var result = DocumentTemplateType.fromDocumentTypeAndRecordType("GLPP", "INDIVIDUAL_PROSPECT");
54+
assertThat(result).isEqualTo(DocumentTemplateType.GLPP);
55+
}
56+
57+
@Test
58+
void given_glpp_and_legal_prospect_should_fail() {
59+
assertThrows(IllegalArgumentException.class,
60+
() -> DocumentTemplateType.fromDocumentTypeAndRecordType("GLPP", "LEGAL_PROSPECT"));
61+
}
62+
```
63+
64+
### Use Approval Testing instead
65+
We can use `Approval Testing` to quickly put legacy code under tests.
66+
67+
Learn more about it [here](https://understandlegacycode.com/approval-tests/).
68+
69+
It is also called : `Characterization Tests` OR `Golden Master`
70+
- Unit testing assertions can be difficult to use and long to write
71+
- Approval tests simplify this by taking a snapshot of the results / confirming that they have not changed at each run
72+
73+
74+
We add [ApprovalTests](https://github.com/approvals/approvaltests.java) dependency in our pom
75+
76+
```xml
77+
<properties>
78+
<approvaltests.version>22.3.2</approvaltests.version>
79+
</properties>
80+
81+
<dependencies>
82+
<dependency>
83+
<groupId>com.approvaltests</groupId>
84+
<artifactId>approvaltests</artifactId>
85+
<version>${approvaltests.version}</version>
86+
</dependency>
87+
</dependencies>
88+
```
89+
90+
- Add `.gitignore` file to exclude `*.received.*` from git
91+
92+
```text
93+
### Approval exclusion ###
94+
*.received.*
95+
```
96+
97+
- Instead, let's use the power of Approval !!!
98+
99+
![cheat sheet](img/approval-testing-cheatsheet.png)
100+
101+
- We can generate combinations and have only 1 test (golden master) using `CombinationApprovals.verifyAllCombinations`
102+
103+
![Use Combinations](img/use-combinations.png)
104+
105+
- We use all the possible values as inputs
106+
- It will make a cross product for the test
107+
108+
```java
109+
@Test
110+
void combinationTests() {
111+
CombinationApprovals.verifyAllCombinations(
112+
DocumentTemplateType::fromDocumentTypeAndRecordType,
113+
new String[]{"AUTP", "AUTM", "DEERPP", "DEERPM", "SPEC", "GLPP", "GLPM"},
114+
new String[]{"INDIVIDUAL_PROSPECT", "LEGAL_PROSPECT", "ALL"}
115+
);
116+
}
117+
```
118+
119+
- On the first run, 2 files are created
120+
- `DocumentTests.combinationTests.received.txt`: the result of the call of the method under test
121+
- `DocumentTests.combinationTests.approved.txt`: the approved version of the result (approved manually)
122+
123+
![2 files](img/2-files.png)
124+
125+
- The library simply compare the 2 text files, so it fails the first time you run it
126+
127+
![Fail on the first run](img/fail.png)
128+
129+
- It compares the actual result and an empty file
130+
131+
![Compare files](img/file-compare.png)
132+
133+
- We need to approve the `received` file to make the test passes
134+
- Meaning we create the `approved` one with the result of the current production code
135+
136+
```bash
137+
cp src/test/java/DocumentTests.combinationTests.received.txt src/test/java/DocumentTests.combinationTests.approved.txt
138+
```
139+
140+
### Refactoring time
141+
- We can even improve the test by making it totally dynamic
142+
- If we add a new Enum entry the test will fail
143+
- Forcing us to approve the new version of the test output
144+
145+
```java
146+
@Test
147+
void combinationTests() {
148+
verifyAllCombinations(
149+
DocumentTemplateType::fromDocumentTypeAndRecordType,
150+
Arrays.stream(DocumentTemplateType.values()).map(Enum::name).toArray(String[]::new),
151+
Arrays.stream(RecordType.values()).map(Enum::name).toArray(String[]::new)
152+
);
153+
}
154+
```
155+
156+
- In just a few minutes, we have successfully covered a cryptic code with robust tests
157+
158+
![Code Coverage](img/code-coverage.png)
159+
160+
> We are now ready for refactoring... 😉
161+
162+
- We refactor the production code
163+
164+
```java
165+
private static final Map<String, DocumentTemplateType> mapping =
166+
List.of(DocumentTemplateType.values())
167+
.toMap(v -> formatKey(v.getDocumentType(), v.getRecordType().name()), v -> v)
168+
.merge(List.of(RecordType.values()).toMap(v -> formatKey(SPEC.name(), v.name()), v -> SPEC));
169+
170+
private static String formatKey(String documentType, String recordType) {
171+
return documentType.toUpperCase() + "-" + recordType.toUpperCase();
172+
}
173+
174+
public static DocumentTemplateType fromDocumentTypeAndRecordType(String documentType, String recordType) {
175+
return mapping.get(formatKey(documentType, recordType))
176+
.getOrElseThrow(() -> new IllegalArgumentException("Invalid Document template type or record type"));
177+
}
178+
```

solution/day15/pom.xml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="http://maven.apache.org/POM/4.0.0"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>com.advent-of-craft</groupId>
9+
<artifactId>advent-of-craft2023</artifactId>
10+
<version>1.0-SNAPSHOT</version>
11+
</parent>
12+
13+
<artifactId>documents</artifactId>
14+
<version>1.0-SNAPSHOT</version>
15+
<properties>
16+
<approvaltests.version>22.3.2</approvaltests.version>
17+
<vavr.version>0.10.4</vavr.version>
18+
</properties>
19+
20+
<dependencies>
21+
<dependency>
22+
<groupId>com.approvaltests</groupId>
23+
<artifactId>approvaltests</artifactId>
24+
<version>${approvaltests.version}</version>
25+
</dependency>
26+
<dependency>
27+
<groupId>io.vavr</groupId>
28+
<artifactId>vavr</artifactId>
29+
<version>${vavr.version}</version>
30+
</dependency>
31+
</dependencies>
32+
</project>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package document;
2+
3+
import io.vavr.collection.List;
4+
import io.vavr.collection.Map;
5+
6+
public enum DocumentTemplateType {
7+
DEERPP("DEER", RecordType.INDIVIDUAL_PROSPECT),
8+
DEERPM("DEER", RecordType.LEGAL_PROSPECT),
9+
AUTP("AUTP", RecordType.INDIVIDUAL_PROSPECT),
10+
AUTM("AUTM", RecordType.LEGAL_PROSPECT),
11+
SPEC("SPEC", RecordType.ALL),
12+
GLPP("GLPP", RecordType.INDIVIDUAL_PROSPECT),
13+
GLPM("GLPM", RecordType.LEGAL_PROSPECT);
14+
15+
private static final Map<String, DocumentTemplateType> mapping =
16+
List.of(DocumentTemplateType.values())
17+
.toMap(v -> formatKey(v.getDocumentType(), v.getRecordType().name()), v -> v)
18+
.merge(List.of(RecordType.values()).toMap(v -> formatKey(SPEC.name(), v.name()), v -> SPEC));
19+
20+
private final String documentType;
21+
private final RecordType recordType;
22+
23+
DocumentTemplateType(String documentType, RecordType recordType) {
24+
this.documentType = documentType;
25+
this.recordType = recordType;
26+
}
27+
28+
private static String formatKey(String documentType, String recordType) {
29+
return documentType.toUpperCase() + "-" + recordType.toUpperCase();
30+
}
31+
32+
public static DocumentTemplateType fromDocumentTypeAndRecordType(String documentType, String recordType) {
33+
return mapping.get(formatKey(documentType, recordType))
34+
.getOrElseThrow(() -> new IllegalArgumentException("Invalid Document template type or record type"));
35+
}
36+
37+
private RecordType getRecordType() {
38+
return recordType;
39+
}
40+
41+
private String getDocumentType() {
42+
return documentType;
43+
}
44+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package document;
2+
3+
public enum RecordType {
4+
INDIVIDUAL_PROSPECT("IndividualPersonProspect"),
5+
LEGAL_PROSPECT("LegalEntityProspect"),
6+
ALL("All");
7+
private final String value;
8+
9+
RecordType(String value) {
10+
this.value = value;
11+
}
12+
13+
public String getValue() {
14+
return value;
15+
}
16+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[DEERPP, INDIVIDUAL_PROSPECT] => java.lang.IllegalArgumentException: Invalid Document template type or record type
2+
[DEERPP, LEGAL_PROSPECT] => java.lang.IllegalArgumentException: Invalid Document template type or record type
3+
[DEERPP, ALL] => java.lang.IllegalArgumentException: Invalid Document template type or record type
4+
[DEERPM, INDIVIDUAL_PROSPECT] => java.lang.IllegalArgumentException: Invalid Document template type or record type
5+
[DEERPM, LEGAL_PROSPECT] => java.lang.IllegalArgumentException: Invalid Document template type or record type
6+
[DEERPM, ALL] => java.lang.IllegalArgumentException: Invalid Document template type or record type
7+
[AUTP, INDIVIDUAL_PROSPECT] => AUTP
8+
[AUTP, LEGAL_PROSPECT] => java.lang.IllegalArgumentException: Invalid Document template type or record type
9+
[AUTP, ALL] => java.lang.IllegalArgumentException: Invalid Document template type or record type
10+
[AUTM, INDIVIDUAL_PROSPECT] => java.lang.IllegalArgumentException: Invalid Document template type or record type
11+
[AUTM, LEGAL_PROSPECT] => AUTM
12+
[AUTM, ALL] => java.lang.IllegalArgumentException: Invalid Document template type or record type
13+
[SPEC, INDIVIDUAL_PROSPECT] => SPEC
14+
[SPEC, LEGAL_PROSPECT] => SPEC
15+
[SPEC, ALL] => SPEC
16+
[GLPP, INDIVIDUAL_PROSPECT] => GLPP
17+
[GLPP, LEGAL_PROSPECT] => java.lang.IllegalArgumentException: Invalid Document template type or record type
18+
[GLPP, ALL] => java.lang.IllegalArgumentException: Invalid Document template type or record type
19+
[GLPM, INDIVIDUAL_PROSPECT] => java.lang.IllegalArgumentException: Invalid Document template type or record type
20+
[GLPM, LEGAL_PROSPECT] => GLPM
21+
[GLPM, ALL] => java.lang.IllegalArgumentException: Invalid Document template type or record type
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import document.DocumentTemplateType;
2+
import document.RecordType;
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.Arrays;
6+
7+
import static org.approvaltests.combinations.CombinationApprovals.verifyAllCombinations;
8+
9+
class DocumentTests {
10+
@Test
11+
void combinationTests() {
12+
verifyAllCombinations(
13+
DocumentTemplateType::fromDocumentTypeAndRecordType,
14+
Arrays.stream(DocumentTemplateType.values()).map(Enum::name).toArray(String[]::new),
15+
Arrays.stream(RecordType.values()).map(Enum::name).toArray(String[]::new)
16+
);
17+
}
18+
}

solution/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<module>day12</module>
2424
<module>day13</module>
2525
<module>day14</module>
26+
<module>day15</module>
2627
</modules>
2728

2829
<properties>

0 commit comments

Comments
 (0)