Skip to content

Commit

Permalink
Proposed Solution Day 15.
Browse files Browse the repository at this point in the history
ythirion committed Dec 16, 2023
1 parent edff26f commit ec9f56a
Showing 17 changed files with 360 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -79,7 +79,7 @@ A solution proposal will be published here every day during the `Advent Of Craft
- [Day 12: Make your code open for extension.](solution/day12/docs/step-by-step.md)
- [Day 13: Find a way to eliminate the irrelevant, and amplify the essentials of those tests.](solution/day13/docs/step-by-step.md)
- [Day 14: Do not use exceptions anymore.](solution/day14/docs/step-by-step.md)

- [Day 15: Put a code under tests.](solution/day15/docs/step-by-step.md)

## Contributors

49 changes: 49 additions & 0 deletions solution/day15/docs/challenge-done.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
## Day 15: Put a code under tests.

Imagine we need to adapt the code below to support a new document template
- We want to be sure to not introduce regression / have a safety net

### Assessing the right type of tests

- Let's add some tests
- We have plenty of possible combinations
- We could use `ParameterizedTests` to make those assertions

### Use Approval Testing instead

We can use `Approval Testing` to quickly put legacy code under tests.

It is also called : `Characterization Tests` OR `Golden Master`
- Unit testing assertions can be difficult to use and long to write
- Approval tests simplify this by taking a snapshot of the results / confirming that they have not changed at each run

Let's set that up:
- We add [ApprovalTests](https://github.com/approvals/approvaltests.java) dependency in our pom
- Add `.gitignore` file to exclude `*.received.*` from git
- Instead, let's use the power of Approval !!!
- We can generate combinations and have only 1 test (golden master) using `CombinationApprovals.verifyAllCombinations`

How it works:
- On the first run, 2 files are created
- `DocumentTests.combinationTests.received.txt`: the result of the call of the method under test
- `DocumentTests.combinationTests.approved.txt`: the approved version of the result (approved manually)
- The library simply compare the 2 text files, so it fails the first time you run it
- It compares the actual result and an empty file

- We need to approve the `received` file to make the test passes
- Meaning we create the `approved` one with the result of the current production code

### Refactoring time
- We can even improve the test by making it totally dynamic
- If we add a new Enum entry the test will fail
- Forcing us to approve the new version of the test output

- In just a few minutes, we have successfully covered a cryptic code with robust tests

>**Tip of the day: Choosing the right type of test can help you gather better feedback on your code.**
### Share your experience

How does your code look like?

Please let everyone know in the discord.
Binary file added solution/day15/docs/img/2-files.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added solution/day15/docs/img/code-coverage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added solution/day15/docs/img/fail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added solution/day15/docs/img/file-compare.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added solution/day15/docs/img/first-run.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added solution/day15/docs/img/use-combinations.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added solution/day15/docs/snippet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
178 changes: 178 additions & 0 deletions solution/day15/docs/step-by-step.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
## Day 15: Put a code under tests.
- Imagine we need to adapt the code below to support a new document template
- We want to be sure to not introduce regression / have a safety net

```java
public enum DocumentTemplateType {
DRP("DEER", I),
DPM("DEER", L),
ATP("AUTP", I),
ATM("AUTM", L),
SPEC("SPEC", ALL),
GLP("GLPP", I),
GLM("GLPM", L);

private final String documentType;
private final RecordType recordType;

DocumentTemplateType(String documentType, RecordType recordType) {
this.documentType = documentType;
this.recordType = recordType;
}

public static DocumentTemplateType fromDocumentTypeAndRecordType(String documentType, String recordType) {
for (DocumentTemplateType dtt : DocumentTemplateType.values()) {
if (dtt.getDocumentType().equalsIgnoreCase(documentType)
&& dtt.getRecordType().equals(RecordType.valueOf(recordType))) {
return dtt;
} else if (dtt.getDocumentType().equalsIgnoreCase(documentType)
&& dtt.getRecordType().equals(ALL)) {
return dtt;
}
}
throw new IllegalArgumentException("Invalid Document template type or record type");
}

private RecordType getRecordType() {
return recordType;
}

private String getDocumentType() {
return documentType;
}
}
```

- Let's add some tests
- We have plenty of possible combinations
- We could use `ParameterizedTests` to make those assertions

```java
@Test
void given_glpp_and_individual_prospect_should_return_glpp() {
final var result = DocumentTemplateType.fromDocumentTypeAndRecordType("GLPP", "INDIVIDUAL_PROSPECT");
assertThat(result).isEqualTo(DocumentTemplateType.GLPP);
}

@Test
void given_glpp_and_legal_prospect_should_fail() {
assertThrows(IllegalArgumentException.class,
() -> DocumentTemplateType.fromDocumentTypeAndRecordType("GLPP", "LEGAL_PROSPECT"));
}
```

### Use Approval Testing instead
We can use `Approval Testing` to quickly put legacy code under tests.

Learn more about it [here](https://understandlegacycode.com/approval-tests/).

It is also called : `Characterization Tests` OR `Golden Master`
- Unit testing assertions can be difficult to use and long to write
- Approval tests simplify this by taking a snapshot of the results / confirming that they have not changed at each run


We add [ApprovalTests](https://github.com/approvals/approvaltests.java) dependency in our pom

```xml
<properties>
<approvaltests.version>22.3.2</approvaltests.version>
</properties>

<dependencies>
<dependency>
<groupId>com.approvaltests</groupId>
<artifactId>approvaltests</artifactId>
<version>${approvaltests.version}</version>
</dependency>
</dependencies>
```

- Add `.gitignore` file to exclude `*.received.*` from git

```text
### Approval exclusion ###
*.received.*
```

- Instead, let's use the power of Approval !!!

![cheat sheet](img/approval-testing-cheatsheet.png)

- We can generate combinations and have only 1 test (golden master) using `CombinationApprovals.verifyAllCombinations`

![Use Combinations](img/use-combinations.png)

- We use all the possible values as inputs
- It will make a cross product for the test

```java
@Test
void combinationTests() {
CombinationApprovals.verifyAllCombinations(
DocumentTemplateType::fromDocumentTypeAndRecordType,
new String[]{"AUTP", "AUTM", "DEERPP", "DEERPM", "SPEC", "GLPP", "GLPM"},
new String[]{"INDIVIDUAL_PROSPECT", "LEGAL_PROSPECT", "ALL"}
);
}
```

- On the first run, 2 files are created
- `DocumentTests.combinationTests.received.txt`: the result of the call of the method under test
- `DocumentTests.combinationTests.approved.txt`: the approved version of the result (approved manually)

![2 files](img/2-files.png)

- The library simply compare the 2 text files, so it fails the first time you run it

![Fail on the first run](img/fail.png)

- It compares the actual result and an empty file

![Compare files](img/file-compare.png)

- We need to approve the `received` file to make the test passes
- Meaning we create the `approved` one with the result of the current production code

```bash
cp src/test/java/DocumentTests.combinationTests.received.txt src/test/java/DocumentTests.combinationTests.approved.txt
```

### Refactoring time
- We can even improve the test by making it totally dynamic
- If we add a new Enum entry the test will fail
- Forcing us to approve the new version of the test output

```java
@Test
void combinationTests() {
verifyAllCombinations(
DocumentTemplateType::fromDocumentTypeAndRecordType,
Arrays.stream(DocumentTemplateType.values()).map(Enum::name).toArray(String[]::new),
Arrays.stream(RecordType.values()).map(Enum::name).toArray(String[]::new)
);
}
```

- In just a few minutes, we have successfully covered a cryptic code with robust tests

![Code Coverage](img/code-coverage.png)

> We are now ready for refactoring... 😉

- We refactor the production code

```java
private static final Map<String, DocumentTemplateType> mapping =
List.of(DocumentTemplateType.values())
.toMap(v -> formatKey(v.getDocumentType(), v.getRecordType().name()), v -> v)
.merge(List.of(RecordType.values()).toMap(v -> formatKey(SPEC.name(), v.name()), v -> SPEC));

private static String formatKey(String documentType, String recordType) {
return documentType.toUpperCase() + "-" + recordType.toUpperCase();
}

public static DocumentTemplateType fromDocumentTypeAndRecordType(String documentType, String recordType) {
return mapping.get(formatKey(documentType, recordType))
.getOrElseThrow(() -> new IllegalArgumentException("Invalid Document template type or record type"));
}
```
32 changes: 32 additions & 0 deletions solution/day15/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.advent-of-craft</groupId>
<artifactId>advent-of-craft2023</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>documents</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<approvaltests.version>22.3.2</approvaltests.version>
<vavr.version>0.10.4</vavr.version>
</properties>

<dependencies>
<dependency>
<groupId>com.approvaltests</groupId>
<artifactId>approvaltests</artifactId>
<version>${approvaltests.version}</version>
</dependency>
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>${vavr.version}</version>
</dependency>
</dependencies>
</project>
44 changes: 44 additions & 0 deletions solution/day15/src/main/java/document/DocumentTemplateType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package document;

import io.vavr.collection.List;
import io.vavr.collection.Map;

public enum DocumentTemplateType {
DEERPP("DEER", RecordType.INDIVIDUAL_PROSPECT),
DEERPM("DEER", RecordType.LEGAL_PROSPECT),
AUTP("AUTP", RecordType.INDIVIDUAL_PROSPECT),
AUTM("AUTM", RecordType.LEGAL_PROSPECT),
SPEC("SPEC", RecordType.ALL),
GLPP("GLPP", RecordType.INDIVIDUAL_PROSPECT),
GLPM("GLPM", RecordType.LEGAL_PROSPECT);

private static final Map<String, DocumentTemplateType> mapping =
List.of(DocumentTemplateType.values())
.toMap(v -> formatKey(v.getDocumentType(), v.getRecordType().name()), v -> v)
.merge(List.of(RecordType.values()).toMap(v -> formatKey(SPEC.name(), v.name()), v -> SPEC));

private final String documentType;
private final RecordType recordType;

DocumentTemplateType(String documentType, RecordType recordType) {
this.documentType = documentType;
this.recordType = recordType;
}

private static String formatKey(String documentType, String recordType) {
return documentType.toUpperCase() + "-" + recordType.toUpperCase();
}

public static DocumentTemplateType fromDocumentTypeAndRecordType(String documentType, String recordType) {
return mapping.get(formatKey(documentType, recordType))
.getOrElseThrow(() -> new IllegalArgumentException("Invalid Document template type or record type"));
}

private RecordType getRecordType() {
return recordType;
}

private String getDocumentType() {
return documentType;
}
}
16 changes: 16 additions & 0 deletions solution/day15/src/main/java/document/RecordType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package document;

public enum RecordType {
INDIVIDUAL_PROSPECT("IndividualPersonProspect"),
LEGAL_PROSPECT("LegalEntityProspect"),
ALL("All");
private final String value;

RecordType(String value) {
this.value = value;
}

public String getValue() {
return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[DEERPP, INDIVIDUAL_PROSPECT] => java.lang.IllegalArgumentException: Invalid Document template type or record type
[DEERPP, LEGAL_PROSPECT] => java.lang.IllegalArgumentException: Invalid Document template type or record type
[DEERPP, ALL] => java.lang.IllegalArgumentException: Invalid Document template type or record type
[DEERPM, INDIVIDUAL_PROSPECT] => java.lang.IllegalArgumentException: Invalid Document template type or record type
[DEERPM, LEGAL_PROSPECT] => java.lang.IllegalArgumentException: Invalid Document template type or record type
[DEERPM, ALL] => java.lang.IllegalArgumentException: Invalid Document template type or record type
[AUTP, INDIVIDUAL_PROSPECT] => AUTP
[AUTP, LEGAL_PROSPECT] => java.lang.IllegalArgumentException: Invalid Document template type or record type
[AUTP, ALL] => java.lang.IllegalArgumentException: Invalid Document template type or record type
[AUTM, INDIVIDUAL_PROSPECT] => java.lang.IllegalArgumentException: Invalid Document template type or record type
[AUTM, LEGAL_PROSPECT] => AUTM
[AUTM, ALL] => java.lang.IllegalArgumentException: Invalid Document template type or record type
[SPEC, INDIVIDUAL_PROSPECT] => SPEC
[SPEC, LEGAL_PROSPECT] => SPEC
[SPEC, ALL] => SPEC
[GLPP, INDIVIDUAL_PROSPECT] => GLPP
[GLPP, LEGAL_PROSPECT] => java.lang.IllegalArgumentException: Invalid Document template type or record type
[GLPP, ALL] => java.lang.IllegalArgumentException: Invalid Document template type or record type
[GLPM, INDIVIDUAL_PROSPECT] => java.lang.IllegalArgumentException: Invalid Document template type or record type
[GLPM, LEGAL_PROSPECT] => GLPM
[GLPM, ALL] => java.lang.IllegalArgumentException: Invalid Document template type or record type
18 changes: 18 additions & 0 deletions solution/day15/src/test/java/DocumentTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import document.DocumentTemplateType;
import document.RecordType;
import org.junit.jupiter.api.Test;

import java.util.Arrays;

import static org.approvaltests.combinations.CombinationApprovals.verifyAllCombinations;

class DocumentTests {
@Test
void combinationTests() {
verifyAllCombinations(
DocumentTemplateType::fromDocumentTypeAndRecordType,
Arrays.stream(DocumentTemplateType.values()).map(Enum::name).toArray(String[]::new),
Arrays.stream(RecordType.values()).map(Enum::name).toArray(String[]::new)
);
}
}
1 change: 1 addition & 0 deletions solution/pom.xml
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@
<module>day12</module>
<module>day13</module>
<module>day14</module>
<module>day15</module>
</modules>

<properties>

0 comments on commit ec9f56a

Please sign in to comment.