diff --git a/README.md b/README.md
index ae6dcf31..5ef72c62 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/solution/day15/docs/challenge-done.md b/solution/day15/docs/challenge-done.md
new file mode 100644
index 00000000..865c48c9
--- /dev/null
+++ b/solution/day15/docs/challenge-done.md
@@ -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.
diff --git a/solution/day15/docs/img/2-files.png b/solution/day15/docs/img/2-files.png
new file mode 100644
index 00000000..d9c5d24b
Binary files /dev/null and b/solution/day15/docs/img/2-files.png differ
diff --git a/solution/day15/docs/img/approval-testing-cheatsheet.png b/solution/day15/docs/img/approval-testing-cheatsheet.png
new file mode 100644
index 00000000..c9dbefe2
Binary files /dev/null and b/solution/day15/docs/img/approval-testing-cheatsheet.png differ
diff --git a/solution/day15/docs/img/code-coverage.png b/solution/day15/docs/img/code-coverage.png
new file mode 100644
index 00000000..c589b098
Binary files /dev/null and b/solution/day15/docs/img/code-coverage.png differ
diff --git a/solution/day15/docs/img/fail.png b/solution/day15/docs/img/fail.png
new file mode 100644
index 00000000..2b2b0ded
Binary files /dev/null and b/solution/day15/docs/img/fail.png differ
diff --git a/solution/day15/docs/img/file-compare.png b/solution/day15/docs/img/file-compare.png
new file mode 100644
index 00000000..b0adebd4
Binary files /dev/null and b/solution/day15/docs/img/file-compare.png differ
diff --git a/solution/day15/docs/img/first-run.png b/solution/day15/docs/img/first-run.png
new file mode 100644
index 00000000..00ffe626
Binary files /dev/null and b/solution/day15/docs/img/first-run.png differ
diff --git a/solution/day15/docs/img/use-combinations.png b/solution/day15/docs/img/use-combinations.png
new file mode 100644
index 00000000..5c360009
Binary files /dev/null and b/solution/day15/docs/img/use-combinations.png differ
diff --git a/solution/day15/docs/snippet.png b/solution/day15/docs/snippet.png
new file mode 100644
index 00000000..c91d6686
Binary files /dev/null and b/solution/day15/docs/snippet.png differ
diff --git a/solution/day15/docs/step-by-step.md b/solution/day15/docs/step-by-step.md
new file mode 100644
index 00000000..b0da70c3
--- /dev/null
+++ b/solution/day15/docs/step-by-step.md
@@ -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
+
+ 22.3.2
+
+
+
+
+ com.approvaltests
+ approvaltests
+ ${approvaltests.version}
+
+
+```
+
+- Add `.gitignore` file to exclude `*.received.*` from git
+
+```text
+### Approval exclusion ###
+*.received.*
+```
+
+- Instead, let's use the power of Approval !!!
+
+
+
+- We can generate combinations and have only 1 test (golden master) using `CombinationApprovals.verifyAllCombinations`
+
+
+
+- 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)
+
+
+
+- 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
+
+```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
+
+
+
+> We are now ready for refactoring... 😉
+
+- We refactor the production code
+
+```java
+private static final Map 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"));
+}
+```
\ No newline at end of file
diff --git a/solution/day15/pom.xml b/solution/day15/pom.xml
new file mode 100644
index 00000000..1d597c36
--- /dev/null
+++ b/solution/day15/pom.xml
@@ -0,0 +1,32 @@
+
+
+ 4.0.0
+
+
+ com.advent-of-craft
+ advent-of-craft2023
+ 1.0-SNAPSHOT
+
+
+ documents
+ 1.0-SNAPSHOT
+
+ 22.3.2
+ 0.10.4
+
+
+
+
+ com.approvaltests
+ approvaltests
+ ${approvaltests.version}
+
+
+ io.vavr
+ vavr
+ ${vavr.version}
+
+
+
\ No newline at end of file
diff --git a/solution/day15/src/main/java/document/DocumentTemplateType.java b/solution/day15/src/main/java/document/DocumentTemplateType.java
new file mode 100644
index 00000000..b97912c7
--- /dev/null
+++ b/solution/day15/src/main/java/document/DocumentTemplateType.java
@@ -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 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;
+ }
+}
\ No newline at end of file
diff --git a/solution/day15/src/main/java/document/RecordType.java b/solution/day15/src/main/java/document/RecordType.java
new file mode 100644
index 00000000..259b3c05
--- /dev/null
+++ b/solution/day15/src/main/java/document/RecordType.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/solution/day15/src/test/java/DocumentTests.combinationTests.approved.txt b/solution/day15/src/test/java/DocumentTests.combinationTests.approved.txt
new file mode 100644
index 00000000..61a25237
--- /dev/null
+++ b/solution/day15/src/test/java/DocumentTests.combinationTests.approved.txt
@@ -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
diff --git a/solution/day15/src/test/java/DocumentTests.java b/solution/day15/src/test/java/DocumentTests.java
new file mode 100644
index 00000000..0c5678d6
--- /dev/null
+++ b/solution/day15/src/test/java/DocumentTests.java
@@ -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)
+ );
+ }
+}
diff --git a/solution/pom.xml b/solution/pom.xml
index b66661c7..3853373b 100644
--- a/solution/pom.xml
+++ b/solution/pom.xml
@@ -23,6 +23,7 @@
day12
day13
day14
+ day15