diff --git a/README.md b/README.md index 7b1f65f5..682edd79 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,69 @@ public class HibernateSnapshotSerializer implements SnapshotSerializer { } ``` +## Supplying a custom SnapshotComparator +The comparator determines if two snapshots match. + +Currently, we support one default comparator (`PlainTextEqualsComparator`) which uses string equals for comparison. + +This should work for most cases. Custom implementations of `SnapshotComparator` can provide more advanced comparisons. + +Comparators follow the same resolution order as Serializers +1. method +2. class +3. global + +### Example: JsonObjectComparator +The default comparator may be too strict for certain types of data. +For example, when comparing json objects, formatting of the json string or the order of fields +may not be of much importance during comparison. A custom comparator can help in such cases. + +For example, the following will convert a json string to a Map and then perform an equals comparison +so that formatting and field order are ignored. + +```java +public class JsonObjectComparator implements SnapshotComparator { + @Override + public boolean matches(String snapshotName, String rawSnapshot, String currentObject) { + return asObject(snapshotName, rawSnapshot).equals(asObject(snapshotName, currentObject)); + } + + @SneakyThrows + private static Object asObject(String snapshotName, String json) { + return new ObjectMapper().readValue(json.replaceFirst(snapshotName, ""), Object.class); + } +} +``` + +## Supplying a custom SnapshotReporter +The reporter reports the details of comparison failures. + +Currently, we support one default reporter (`PlainTextSnapshotReporter`) which uses assertj's DiffUtils +to generate a patch of the differences between two snapshots. + +Custom reporters can be plugged in by implementing `SnapshotReporter`. + +Reporters follow the same resolution order as Serializers and Comparators + +### Example: JsonDiffReporter +For generating and reporting json diffs using other libraries like https://github.com/skyscreamer/JSONassert +a custom reporter can be created like the one below. + +```java +public class JsonAssertReporter implements SnapshotReporter { + @Override + public boolean supportsFormat(String outputFormat) { + return SerializerType.JSON.name().equalsIgnoreCase(outputFormat); + } + + @Override + @SneakyThrows + public void report(String snapshotName, String rawSnapshot, String currentObject) { + JSONAssert.assertEquals(rawSnapshot, currentObject, JSONCompareMode.STRICT); + } +} +``` + ## Supplying a custom SnapshotConfig You can override the snapshot configuration easily using the `@UseSnapshotConfig` annotation diff --git a/build.gradle b/build.gradle index 7f6f8b94..ea60e3a3 100644 --- a/build.gradle +++ b/build.gradle @@ -28,10 +28,10 @@ subprojects { subproject -> dependencies { // Lombok - compileOnly 'org.projectlombok:lombok:1.18.8' - annotationProcessor 'org.projectlombok:lombok:1.18.8' - testCompileOnly 'org.projectlombok:lombok:1.18.8' - testAnnotationProcessor 'org.projectlombok:lombok:1.18.8' + compileOnly 'org.projectlombok:lombok:1.18.16' + annotationProcessor 'org.projectlombok:lombok:1.18.16' + testCompileOnly 'org.projectlombok:lombok:1.18.16' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.16' } // Adds verbose logging for TravisCI @@ -41,4 +41,4 @@ subprojects { subproject -> exceptionFormat "full" } } -} \ No newline at end of file +} diff --git a/java-snapshot-testing-core/build.gradle b/java-snapshot-testing-core/build.gradle index bd735ef1..50045ac3 100644 --- a/java-snapshot-testing-core/build.gradle +++ b/java-snapshot-testing-core/build.gradle @@ -12,6 +12,7 @@ dependencies { implementation 'org.assertj:assertj-core:3.11.1' implementation 'org.apache.commons:commons-lang3:3.8.1' implementation 'org.slf4j:slf4j-api:2.0.0-alpha0' + implementation 'org.opentest4j:opentest4j:1.1.1' testImplementation 'org.slf4j:slf4j-simple:2.0.0-alpha0' testImplementation 'org.mockito:mockito-junit-jupiter:2.23.0' @@ -32,4 +33,4 @@ publishing { artifactId 'java-snapshot-testing-core' } } -} \ No newline at end of file +} diff --git a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/Snapshot.java b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/Snapshot.java index 470b402b..bbff771a 100644 --- a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/Snapshot.java +++ b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/Snapshot.java @@ -1,43 +1,61 @@ package au.com.origin.snapshots; +import au.com.origin.snapshots.comparators.SnapshotComparator; import au.com.origin.snapshots.exceptions.SnapshotMatchException; +import au.com.origin.snapshots.reporters.SnapshotReporter; import au.com.origin.snapshots.serializers.DeterministicJacksonSnapshotSerializer; import au.com.origin.snapshots.serializers.JacksonSnapshotSerializer; import au.com.origin.snapshots.serializers.SnapshotSerializer; import au.com.origin.snapshots.serializers.ToStringSnapshotSerializer; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.SneakyThrows; +import lombok.With; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.assertj.core.util.diff.DiffUtils; -import org.assertj.core.util.diff.Patch; import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.Collection; -import java.util.Set; +import java.util.*; +import java.util.stream.Collectors; +@Slf4j +@AllArgsConstructor(access = AccessLevel.PRIVATE) public class Snapshot { - private SnapshotConfig snapshotConfig; - private SnapshotSerializer snapshotSerializer; + private final SnapshotConfig snapshotConfig; + @With(AccessLevel.PACKAGE) private final SnapshotFile snapshotFile; - private final Class testClass; + private final Class testClass; private final Method testMethod; + @With(AccessLevel.PACKAGE) private final Object[] current; + private final boolean isCI; - private String scenario = null; + @With + private final SnapshotSerializer snapshotSerializer; + @With + private final SnapshotComparator snapshotComparator; + @With + private final List snapshotReporters; + @With + private final String scenario; Snapshot( SnapshotConfig snapshotConfig, SnapshotFile snapshotFile, - Class testClass, + Class testClass, Method testMethod, Object... current) { - this.snapshotConfig = snapshotConfig; - this.snapshotSerializer = snapshotConfig.getSerializer(); - this.current = current; - this.snapshotFile = snapshotFile; - this.testClass = testClass; - this.testMethod = testMethod; + this(snapshotConfig, + snapshotFile, + testClass, + testMethod, + current, + snapshotConfig.isCI(), + snapshotConfig.getSerializer(), + snapshotConfig.getComparator(), + snapshotConfig.getReporters(), + null); } /** @@ -50,8 +68,7 @@ public class Snapshot { * @return Snapshot */ public Snapshot scenario(String scenario) { - this.scenario = scenario; - return this; + return this.withScenario(scenario); } /** @@ -61,8 +78,28 @@ public Snapshot scenario(String scenario) { * @return Snapshot */ public Snapshot serializer(SnapshotSerializer serializer) { - this.snapshotSerializer = serializer; - return this; + return this.withSnapshotSerializer(serializer); + } + + /** + * Apply a custom comparator for this snapshot + * + * @param comparator your custom comparator + * @return Snapshot + */ + public Snapshot comparator(SnapshotComparator comparator) { + return this.withSnapshotComparator(comparator); + } + + /** + * Apply a list of custom reporters for this snapshot + * This will replace the default reporters defined in the config + * + * @param reporters your custom reporters + * @return Snapshot + */ + public Snapshot reporters(SnapshotReporter... reporters) { + return this.withSnapshotReporters(Arrays.asList(reporters)); } /** @@ -93,12 +130,11 @@ public Snapshot orderedJson() { * Apply a custom serializer for this snapshot * * @param serializer your custom serializer - * @return this + * @return this */ @SneakyThrows public Snapshot serializer(Class serializer) { - this.snapshotSerializer = serializer.newInstance(); - return this; + return this.withSnapshotSerializer(serializer.getConstructor().newInstance()); } public void toMatchSnapshot() { @@ -116,13 +152,42 @@ public void toMatchSnapshot() { if (rawSnapshot != null) { // Match existing Snapshot - if (!rawSnapshot.trim().equals(currentObject.trim())) { + if (!snapshotComparator.matches(getSnapshotName(), rawSnapshot, currentObject)) { snapshotFile.createDebugFile(currentObject.trim()); - throw generateDiffError(rawSnapshot, currentObject); + + List reporters = snapshotReporters + .stream() + .filter(reporter -> reporter.supportsFormat(snapshotSerializer.getOutputFormat())) + .collect(Collectors.toList()); + + if (reporters.isEmpty()) { + String comparator = snapshotComparator.getClass().getSimpleName(); + throw new IllegalStateException("No compatible reporters found for comparator " + comparator); + } + + List errors = new ArrayList<>(); + + for (SnapshotReporter reporter : reporters) { + try { + reporter.report(getSnapshotName(), rawSnapshot, currentObject); + } catch (Throwable t) { + errors.add(t); + } + } + + if(!errors.isEmpty()) { + throw new SnapshotMatchException("Error(s) matching snapshot(s)", errors); + } } } else { - // Create New Snapshot - snapshotFile.push(currentObject); + if (this.isCI) { + log.error("We detected you are running on a CI Server - if this is incorrect please override the isCI() method in SnapshotConfig"); + throw new SnapshotMatchException("Snapshot [" + getSnapshotName() + "] not found. Has this snapshot been committed ?"); + } else { + log.warn("We detected you are running on a developer machine - if this is incorrect please override the isCI() method in SnapshotConfig"); + // Create New Snapshot + snapshotFile.push(currentObject); + } } snapshotFile.deleteDebugFile(); } @@ -135,28 +200,8 @@ private boolean shouldUpdateSnapshot() { } } - private SnapshotMatchException generateDiffError(String rawSnapshot, String currentObject) { - // compute the patch: this is the diffutils part - Patch patch = - DiffUtils.diff( - Arrays.asList(rawSnapshot.trim().split("\n")), - Arrays.asList(currentObject.trim().split("\n"))); - String error = - "Error on: \n" - + currentObject.trim() - + "\n\n" - + patch - .getDeltas() - .stream() - .map(delta -> delta.toString() + "\n") - .reduce(String::concat) - .get(); - return new SnapshotMatchException(error); - } - private String getRawSnapshot(Collection rawSnapshots) { for (String rawSnapshot : rawSnapshots) { - if (rawSnapshot.contains(getSnapshotName())) { return rawSnapshot; } diff --git a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/SnapshotConfig.java b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/SnapshotConfig.java index 8bad21e4..66e2ba6c 100644 --- a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/SnapshotConfig.java +++ b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/SnapshotConfig.java @@ -1,18 +1,25 @@ package au.com.origin.snapshots; +import au.com.origin.snapshots.comparators.PlainTextEqualsComparator; +import au.com.origin.snapshots.comparators.SnapshotComparator; +import au.com.origin.snapshots.reporters.PlainTextSnapshotReporter; +import au.com.origin.snapshots.reporters.SnapshotReporter; import au.com.origin.snapshots.serializers.SnapshotSerializer; import au.com.origin.snapshots.serializers.ToStringSnapshotSerializer; +import org.apache.commons.lang3.StringUtils; import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; import java.util.Optional; /** - * Snapshot Configuration - * ---------------------- + * Snapshot Configuration + * ---------------------- * - * Implement this interface when integrating `java-snapshot-testing` with a custom testing library + * Implement this interface when integrating `java-snapshot-testing` with a custom testing library * - * For frameworks - consider extending the framework implementation instead + * For frameworks - consider extending the framework implementation instead */ public interface SnapshotConfig { String JVM_UPDATE_SNAPSHOTS_PARAMETER = "updateSnapshot"; @@ -67,7 +74,7 @@ default String getSnapshotFolder() { * However, some don't and can use this method to discover it. * * @param testClass class under test - * @return method under tests + * @return method under tests */ Method getTestMethod(Class testClass); @@ -77,12 +84,7 @@ default String getSnapshotFolder() { * @return snapshots should be updated automatically without verification */ default Optional updateSnapshot() { - String value = System.getProperty(JVM_UPDATE_SNAPSHOTS_PARAMETER); - if ( value != null) { - return Optional.of(value); - } else { - return Optional.empty(); - } + return Optional.ofNullable(System.getProperty(JVM_UPDATE_SNAPSHOTS_PARAMETER)); } /** @@ -95,7 +97,6 @@ default SnapshotSerializer getSerializer() { return new ToStringSnapshotSerializer(); } - /** * Optional * Allows you to perform any custom modifications to the file before it is saved @@ -107,4 +108,49 @@ default SnapshotSerializer getSerializer() { default String onSaveSnapshotFile(Class testClass, String snapshotContent) { return snapshotContent; } + + /** + * Optional + * Override to supply your own custom comparator function + * + * @return custom comparator function + */ + default SnapshotComparator getComparator() { + return new PlainTextEqualsComparator(); + } + + /** + * Optional + * Override to supply your own custom reporter functions + * Reporters will run in the same sequence as provided. + * Reporters should throw exceptions to indicate comparison failure. + * Exceptions thrown from reporters are aggregated and reported together. + * Reporters that wish to leverage IDE comparison tools can use standard + * assertion libraries like assertj, junit jupiter assertions (or) opentest4j. + * + * @return custom reporter functions + */ + default List getReporters() { + return Collections.singletonList(new PlainTextSnapshotReporter()); + } + + /** + * Optional + * This method is meant to detect if we're running on a CI environment. + * This is used to determine the action to be taken when a snapshot is not found. + * + * If this method returns false, meaning we're NOT running on a CI environment (probably a dev machine), + * a new snapshot is created when not found. + * + * If this method returns true, meaning we're running on a CI environment, no new snapshots are created + * and an error is thrown instead to prevent tests from silently passing when snapshots are not found. + * + * Default logic to determine if running on a CI environment is to check for the presence of a 'CI' env variable + * Override this method if a custom CI detection logic is required. + * + * @return boolean indicating if we're running on a CI environment or not + */ + default boolean isCI() { + return StringUtils.isNotEmpty(System.getenv("CI")); + } } diff --git a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/SnapshotVerifier.java b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/SnapshotVerifier.java index 8bf97e77..10606833 100644 --- a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/SnapshotVerifier.java +++ b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/SnapshotVerifier.java @@ -22,7 +22,7 @@ @RequiredArgsConstructor public class SnapshotVerifier { - private final Class testClass; + private final Class testClass; private final SnapshotFile snapshotFile; private final SnapshotConfig config; private final boolean failOnOrphans; @@ -54,6 +54,7 @@ public void validateSnapshots() { for (String snapshotName : snapshotNames) { if (rawSnapshot.contains(snapshotName)) { foundSnapshot = true; + break; } } if (!foundSnapshot) { diff --git a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/comparators/PlainTextEqualsComparator.java b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/comparators/PlainTextEqualsComparator.java new file mode 100644 index 00000000..e4a9ee91 --- /dev/null +++ b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/comparators/PlainTextEqualsComparator.java @@ -0,0 +1,9 @@ +package au.com.origin.snapshots.comparators; + +public class PlainTextEqualsComparator implements SnapshotComparator { + + @Override + public boolean matches(String snapshotName, String rawSnapshot, String currentObject) { + return rawSnapshot.trim().equals(currentObject.trim()); + } +} diff --git a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/comparators/SnapshotComparator.java b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/comparators/SnapshotComparator.java new file mode 100644 index 00000000..e49dd46c --- /dev/null +++ b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/comparators/SnapshotComparator.java @@ -0,0 +1,5 @@ +package au.com.origin.snapshots.comparators; + +public interface SnapshotComparator { + boolean matches(String snapshotName, String rawSnapshot, String currentObject); +} diff --git a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/exceptions/SnapshotMatchException.java b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/exceptions/SnapshotMatchException.java index 0209c063..78fcaabd 100644 --- a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/exceptions/SnapshotMatchException.java +++ b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/exceptions/SnapshotMatchException.java @@ -1,12 +1,21 @@ package au.com.origin.snapshots.exceptions; -public class SnapshotMatchException extends RuntimeException { +import org.opentest4j.MultipleFailuresError; + +import java.util.Collections; +import java.util.List; + +public class SnapshotMatchException extends MultipleFailuresError { public SnapshotMatchException(String message) { - super(message); + super(message, Collections.emptyList()); } public SnapshotMatchException(String message, Throwable cause) { - super(message, cause); + super(message, Collections.singletonList(cause)); + } + + public SnapshotMatchException(String message, List causes) { + super(message, causes); } } diff --git a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/reporters/PlainTextSnapshotReporter.java b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/reporters/PlainTextSnapshotReporter.java new file mode 100644 index 00000000..1bb6c673 --- /dev/null +++ b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/reporters/PlainTextSnapshotReporter.java @@ -0,0 +1,39 @@ +package au.com.origin.snapshots.reporters; + +import org.assertj.core.util.diff.DiffUtils; +import org.assertj.core.util.diff.Patch; +import org.opentest4j.AssertionFailedError; + +import java.util.Arrays; +import java.util.function.Supplier; + +public class PlainTextSnapshotReporter implements SnapshotReporter { + + private static final Supplier NO_DIFF_EXCEPTION_SUPPLIER = + () -> new IllegalStateException("No differences found. Potential mismatch between comparator and reporter"); + + @Override + public boolean supportsFormat(String outputFormat) { + return true; // always true + } + + @Override + public void report(String snapshotName, String rawSnapshot, String currentObject) { + Patch patch = DiffUtils.diff( + Arrays.asList(rawSnapshot.trim().split("\n")), + Arrays.asList(currentObject.trim().split("\n"))); + + String message = "Error on: \n" + currentObject.trim() + "\n\n" + getDiffString(patch); + + throw new AssertionFailedError(message, rawSnapshot, currentObject); + } + + public static String getDiffString(Patch patch) { + return patch + .getDeltas() + .stream() + .map(delta -> delta.toString() + "\n") + .reduce(String::concat) + .orElseThrow(NO_DIFF_EXCEPTION_SUPPLIER); + } +} diff --git a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/reporters/SnapshotReporter.java b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/reporters/SnapshotReporter.java new file mode 100644 index 00000000..9e2ee0b8 --- /dev/null +++ b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/reporters/SnapshotReporter.java @@ -0,0 +1,8 @@ +package au.com.origin.snapshots.reporters; + +public interface SnapshotReporter { + + boolean supportsFormat(String outputFormat); + + void report(String snapshotName, String rawSnapshot, String currentObject); +} diff --git a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/serializers/JacksonSnapshotSerializer.java b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/serializers/JacksonSnapshotSerializer.java index 1914092b..34f9aa9b 100644 --- a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/serializers/JacksonSnapshotSerializer.java +++ b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/serializers/JacksonSnapshotSerializer.java @@ -69,4 +69,9 @@ public String apply(Object[] objects) { throw new SnapshotExtensionException("Jackson Serialization failed", e); } } + + @Override + public String getOutputFormat() { + return SerializerType.JSON.name(); + } } diff --git a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/serializers/SerializerType.java b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/serializers/SerializerType.java new file mode 100644 index 00000000..e75cc38d --- /dev/null +++ b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/serializers/SerializerType.java @@ -0,0 +1,5 @@ +package au.com.origin.snapshots.serializers; + +public enum SerializerType { + TEXT, JSON; +} diff --git a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/serializers/SnapshotSerializer.java b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/serializers/SnapshotSerializer.java index 4282faef..b54baae2 100644 --- a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/serializers/SnapshotSerializer.java +++ b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/serializers/SnapshotSerializer.java @@ -4,4 +4,5 @@ public interface SnapshotSerializer extends Function { + String getOutputFormat(); } diff --git a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/serializers/ToStringSnapshotSerializer.java b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/serializers/ToStringSnapshotSerializer.java index 0d59182b..b640c7d2 100644 --- a/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/serializers/ToStringSnapshotSerializer.java +++ b/java-snapshot-testing-core/src/main/java/au/com/origin/snapshots/serializers/ToStringSnapshotSerializer.java @@ -9,8 +9,14 @@ * Will render each toString() on a separate line */ public class ToStringSnapshotSerializer implements SnapshotSerializer { + @Override public String apply(Object[] objects) { return "[\n" + Arrays.stream(objects).map(Object::toString).collect(Collectors.joining("\n")) + "\n]"; } + + @Override + public String getOutputFormat() { + return SerializerType.TEXT.name(); + } } diff --git a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/SnapshotTest.java b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/SnapshotTest.java index 44850543..736353b9 100644 --- a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/SnapshotTest.java +++ b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/SnapshotTest.java @@ -2,22 +2,23 @@ import au.com.origin.snapshots.config.BaseSnapshotConfig; import au.com.origin.snapshots.exceptions.SnapshotMatchException; -import au.com.origin.snapshots.serializers.JacksonSnapshotSerializer; -import lombok.SneakyThrows; +import au.com.origin.snapshots.reporters.SnapshotReporter; +import au.com.origin.snapshots.serializers.*; +import lombok.*; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.opentest4j.AssertionFailedError; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; -import java.util.TreeSet; +import java.util.*; +import java.util.function.BiConsumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -125,4 +126,87 @@ void shouldOverwriteSnapshotsWhenParamIsPassed() { Mockito.verify(snapshotFile) .push("java.lang.String.toString[hello world]=[\n" + " \"anyObject\"\n" + "]"); } + + @SneakyThrows + @Test + void shouldFailWhenRunningOnCiWithoutExistingSnapshot() { + BaseSnapshotConfig ciSnapshotConfig = new BaseSnapshotConfig() { + @Override + public boolean isCI() { + return true; + } + }; + + Snapshot ciSnapshot = new Snapshot( + ciSnapshotConfig, + new SnapshotFile(ciSnapshotConfig.getOutputDir(), "blah", this.getClass(), (a, b) -> b), + String.class, + String.class.getDeclaredMethod("toString"), + "anyObject"); + + Assertions.assertThatThrownBy(ciSnapshot::toMatchSnapshot) + .hasMessage("Snapshot [java.lang.String.toString=] not found. Has this snapshot been committed ?"); + } + + @SneakyThrows + @Test + void shouldAggregateMultipleFailures() { + SnapshotFile snapshotFile = Mockito.mock(SnapshotFile.class); + Set set = new HashSet<>(); + set.add("java.lang.String.toString=[\n \"hello\"\n]"); + Mockito.when(snapshotFile.getRawSnapshots()).thenReturn(set); + + Stream> reportingFunctions = Stream.of( + (rawSnapshot, currentObject) -> assertThat(currentObject).isEqualTo(rawSnapshot), // assertj + org.junit.jupiter.api.Assertions::assertEquals, // junit jupiter + (rawSnapshot, currentObject) -> { + String message = String.join(System.lineSeparator(), + "Expected : ", rawSnapshot, "Actual : ", currentObject); + throw new AssertionFailedError(message, rawSnapshot, currentObject); // opentest4j + } + ); + + Stream reporters = reportingFunctions.map(consumer -> new SnapshotReporter() { + @Override + public boolean supportsFormat(String outputFormat) { + return true; + } + + @Override + public void report(String snapshotName, String rawSnapshot, String currentObject) { + consumer.accept(rawSnapshot, currentObject); + } + }); + + Snapshot failingSnapshot = snapshot + .withCurrent(new Object[]{"hola"}) + .withSnapshotFile(snapshotFile) + .serializer(new DeterministicJacksonSnapshotSerializer()) + .reporters(reporters.toArray(SnapshotReporter[]::new)); + + try { + failingSnapshot.toMatchSnapshot(); + } + catch (Throwable m) { + String cleanMessage = m.getMessage() + .replace("<\"", "") + .replace("<", "") + .replaceAll("\n", "") + .replaceAll("\r", "") + .replaceAll("\t", "") + .replace("\">", " ") + .replace(">", " ") + .replace("]", "") + .replace("java.lang.String.toString=[", "") + .replaceAll(" +", " "); + + assertThat(cleanMessage).containsPattern("Expecting.*hola.*to be equal to.*hello.*but was not"); // assertj + assertThat(cleanMessage).containsPattern("expected.*hello.*but was.*hola"); // junit jupiter + assertThat(cleanMessage).containsPattern("Expected.*hello.*Actual.*hola"); // opentest4j + + return; + } + + Assertions.fail("Expected an error to be thrown"); + } } diff --git a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/comparators/PlainTextEqualsComparatorTest.java b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/comparators/PlainTextEqualsComparatorTest.java new file mode 100644 index 00000000..561a7e01 --- /dev/null +++ b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/comparators/PlainTextEqualsComparatorTest.java @@ -0,0 +1,19 @@ +package au.com.origin.snapshots.comparators; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class PlainTextEqualsComparatorTest { + + private static final PlainTextEqualsComparator COMPARATOR = new PlainTextEqualsComparator(); + + @Test + void successfulComparison() { + Assertions.assertThat(COMPARATOR.matches("snap1", "blah", "blah")).isTrue(); + } + + @Test + void failingComparison() { + Assertions.assertThat(COMPARATOR.matches("snap1", "blah", "blahblah")).isFalse(); + } +} diff --git a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/config/BaseSnapshotConfig.java b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/config/BaseSnapshotConfig.java index b4db3541..1995bdb0 100644 --- a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/config/BaseSnapshotConfig.java +++ b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/config/BaseSnapshotConfig.java @@ -66,4 +66,9 @@ public Method getTestMethod(Class testClass) { public SnapshotSerializer getSerializer() { return new JacksonSnapshotSerializer(); } + + @Override + public boolean isCI() { + return false; + } } diff --git a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/config/ToStringSnapshotConfig.java b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/config/ToStringSnapshotConfig.java index 616ecd4c..de44d98f 100644 --- a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/config/ToStringSnapshotConfig.java +++ b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/config/ToStringSnapshotConfig.java @@ -1,5 +1,6 @@ package au.com.origin.snapshots.config; +import au.com.origin.snapshots.serializers.SerializerType; import au.com.origin.snapshots.serializers.SnapshotSerializer; import java.util.Arrays; @@ -9,6 +10,16 @@ public class ToStringSnapshotConfig extends BaseSnapshotConfig { @Override public SnapshotSerializer getSerializer() { - return objects -> Arrays.stream(objects).map(Object::toString).collect(Collectors.joining()); + return new SnapshotSerializer() { + @Override + public String getOutputFormat() { + return SerializerType.TEXT.name(); + } + + @Override + public String apply(Object[] objects) { + return Arrays.stream(objects).map(Object::toString).collect(Collectors.joining()); + } + }; } } diff --git a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/reporters/PlainTextSnapshotReporterTest.java b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/reporters/PlainTextSnapshotReporterTest.java new file mode 100644 index 00000000..b505a714 --- /dev/null +++ b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/reporters/PlainTextSnapshotReporterTest.java @@ -0,0 +1,31 @@ +package au.com.origin.snapshots.reporters; + +import au.com.origin.snapshots.serializers.SerializerType; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +class PlainTextSnapshotReporterTest { + private static final PlainTextSnapshotReporter REPORTER = new PlainTextSnapshotReporter(); + + @Test + void shouldSupportAllFormats() { + Assertions.assertThat(REPORTER.supportsFormat(SerializerType.TEXT.name())).isTrue(); + Assertions.assertThat(REPORTER.supportsFormat(SerializerType.JSON.name())).isTrue(); + + Assertions.assertThat(REPORTER.supportsFormat("xml")).isTrue(); + Assertions.assertThat(REPORTER.supportsFormat("blah")).isTrue(); + } + + @Test + void doReport() { + assertThatExceptionOfType(AssertionFailedError.class) + .isThrownBy(() -> REPORTER.report("snap1", "blah", "bloo")) + .withMessageContaining("expecting:") + .withMessageContaining("[\"blah\"]") + .withMessageContaining("but was:") + .withMessageContaining("[\"bloo\"]"); + } +} diff --git a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/DeterministicJacksonSnapshotSerializerTest.java b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/DeterministicJacksonSnapshotSerializerTest.java index efc1508c..9e9e4073 100644 --- a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/DeterministicJacksonSnapshotSerializerTest.java +++ b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/DeterministicJacksonSnapshotSerializerTest.java @@ -2,6 +2,7 @@ import au.com.origin.snapshots.SnapshotConfig; import au.com.origin.snapshots.config.BaseSnapshotConfig; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import java.time.Instant; @@ -28,6 +29,11 @@ public void shouldSerializeDifferentTypes() { validateSnapshots(); } + @Test + void shouldSupportJsonFormat() { + Assertions.assertThat(new DeterministicJacksonSnapshotSerializer().getOutputFormat()).isEqualTo(SerializerType.JSON.name()); + } + private enum AnEnum { F, A, D, E, G, B, C; } @@ -47,7 +53,7 @@ private final class TypeDummy { private final Date date = Date.from(Instant.parse("2020-10-19T22:21:07.103Z")); private final LocalDate localDate = LocalDate.parse("2020-10-19"); private final LocalDateTime localDateTime = LocalDateTime.parse("2020-10-19T22:21:07.103"); - private final ZonedDateTime zonedDateTime = ZonedDateTime.parse("2020-10-19T22:21:07.103+10:00[Australia/Melbourne]"); + private final ZonedDateTime zonedDateTime = ZonedDateTime.parse("2020-04-19T22:21:07.103+10:00[Australia/Melbourne]"); private final AnEnum anEnum = AnEnum.A; private final Optional presentOptional = Optional.of("Hello World"); private final Optional emptyOptional = Optional.empty(); diff --git a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/JacksonSnapshotSerializerTest.java b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/JacksonSnapshotSerializerTest.java index 64fc56d3..74f3b399 100644 --- a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/JacksonSnapshotSerializerTest.java +++ b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/JacksonSnapshotSerializerTest.java @@ -2,6 +2,7 @@ import au.com.origin.snapshots.SnapshotConfig; import au.com.origin.snapshots.config.BaseSnapshotConfig; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import java.time.Instant; @@ -28,6 +29,11 @@ public void shouldSerializeDifferentTypes() { validateSnapshots(); } + @Test + void shouldSupportJsonFormat() { + Assertions.assertThat(new JacksonSnapshotSerializer().getOutputFormat()).isEqualTo(SerializerType.JSON.name()); + } + private enum AnEnum { F, A, D, E, G, B, C; } @@ -47,7 +53,7 @@ private final class TypeDummy { private final Date date = Date.from(Instant.parse("2020-10-19T22:21:07.103Z")); private final LocalDate localDate = LocalDate.parse("2020-10-19"); private final LocalDateTime localDateTime = LocalDateTime.parse("2020-10-19T22:21:07.103"); - private final ZonedDateTime zonedDateTime = ZonedDateTime.parse("2020-10-19T22:21:07.103+10:00[Australia/Melbourne]"); + private final ZonedDateTime zonedDateTime = ZonedDateTime.parse("2020-04-19T22:21:07.103+10:00[Australia/Melbourne]"); private final AnEnum anEnum = AnEnum.A; private final Optional presentOptional = Optional.of("Hello World"); private final Optional emptyOptional = Optional.empty(); diff --git a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/LowercaseToStringSerializer.java b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/LowercaseToStringSerializer.java index 911dee44..ddd4c0e9 100644 --- a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/LowercaseToStringSerializer.java +++ b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/LowercaseToStringSerializer.java @@ -8,4 +8,9 @@ public class LowercaseToStringSerializer implements SnapshotSerializer { public String apply(Object[] objects) { return Arrays.stream(objects).map(Object::toString).collect(Collectors.joining()).toLowerCase(); } + + @Override + public String getOutputFormat() { + return SerializerType.TEXT.name(); + } } diff --git a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/ToStringSnapshotSerializerTest.java b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/ToStringSnapshotSerializerTest.java index ac6d2c29..935d6ffb 100644 --- a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/ToStringSnapshotSerializerTest.java +++ b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/ToStringSnapshotSerializerTest.java @@ -5,6 +5,7 @@ import au.com.origin.snapshots.config.BaseSnapshotConfig; import lombok.AllArgsConstructor; import lombok.Data; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -33,6 +34,11 @@ void canUseSnapshotConfigAnnotationAtClassLevel() { validateSnapshots(); } + @Test + void shouldSupportStringFormat() { + Assertions.assertThat(new ToStringSnapshotSerializer().getOutputFormat()).isEqualTo(SerializerType.TEXT.name()); + } + @AllArgsConstructor @Data private static class Dummy { diff --git a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/UppercaseToStringSerializer.java b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/UppercaseToStringSerializer.java index d95cb82a..843b14e7 100644 --- a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/UppercaseToStringSerializer.java +++ b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/UppercaseToStringSerializer.java @@ -8,4 +8,9 @@ public class UppercaseToStringSerializer implements SnapshotSerializer { public String apply(Object[] objects) { return Arrays.stream(objects).map(Object::toString).collect(Collectors.joining()).toUpperCase(); } + + @Override + public String getOutputFormat() { + return SerializerType.TEXT.name(); + } } diff --git a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/__snapshots__/DeterministicJacksonSnapshotSerializerTest.snap b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/__snapshots__/DeterministicJacksonSnapshotSerializerTest.snap index f272f312..3eb24a10 100644 --- a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/__snapshots__/DeterministicJacksonSnapshotSerializerTest.snap +++ b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/__snapshots__/DeterministicJacksonSnapshotSerializerTest.snap @@ -135,6 +135,6 @@ au.com.origin.snapshots.serializers.DeterministicJacksonSnapshotSerializerTest.s "f", "g" ], - "zonedDateTime": "2020-10-19T22:21:07.103+11:00[Australia/Melbourne]" + "zonedDateTime": "2020-04-19T22:21:07.103+10:00[Australia/Melbourne]" } -] \ No newline at end of file +] diff --git a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/__snapshots__/JacksonSnapshotSerializerTest.snap b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/__snapshots__/JacksonSnapshotSerializerTest.snap index eb7be51d..74c96599 100644 --- a/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/__snapshots__/JacksonSnapshotSerializerTest.snap +++ b/java-snapshot-testing-core/src/test/java/au/com/origin/snapshots/serializers/__snapshots__/JacksonSnapshotSerializerTest.snap @@ -13,7 +13,7 @@ au.com.origin.snapshots.serializers.JacksonSnapshotSerializerTest.shouldSerializ "date": "2020-10-19T22:21:07.103+00:00", "localDate": "2020-10-19", "localDateTime": "2020-10-19T22:21:07.103", - "zonedDateTime": "2020-10-19T22:21:07.103+11:00[Australia/Melbourne]", + "zonedDateTime": "2020-04-19T22:21:07.103+10:00[Australia/Melbourne]", "anEnum": "A", "presentOptional": "Hello World", "emptyOptional": null, @@ -137,4 +137,4 @@ au.com.origin.snapshots.serializers.JacksonSnapshotSerializerTest.shouldSerializ ] ] } -] \ No newline at end of file +]