Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 5 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -41,4 +41,4 @@ subprojects { subproject ->
exceptionFormat "full"
}
}
}
}
3 changes: 2 additions & 1 deletion java-snapshot-testing-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -32,4 +33,4 @@ publishing {
artifactId 'java-snapshot-testing-core'
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<SnapshotReporter> 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);
}

/**
Expand All @@ -50,8 +68,7 @@ public class Snapshot {
* @return Snapshot
*/
public Snapshot scenario(String scenario) {
this.scenario = scenario;
return this;
return this.withScenario(scenario);
}

/**
Expand All @@ -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));
}

/**
Expand Down Expand Up @@ -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<? extends SnapshotSerializer> serializer) {
this.snapshotSerializer = serializer.newInstance();
return this;
return this.withSnapshotSerializer(serializer.getConstructor().newInstance());
}

public void toMatchSnapshot() {
Expand All @@ -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<SnapshotReporter> 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<Throwable> 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();
}
Expand All @@ -135,28 +200,8 @@ private boolean shouldUpdateSnapshot() {
}
}

private SnapshotMatchException generateDiffError(String rawSnapshot, String currentObject) {
// compute the patch: this is the diffutils part
Patch<String> 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<String> rawSnapshots) {
for (String rawSnapshot : rawSnapshots) {

if (rawSnapshot.contains(getSnapshotName())) {
return rawSnapshot;
}
Expand Down
Loading