Skip to content

Commit 73647fb

Browse files
author
Botond Somogyi
committed
#patch: add Jackson 3 snapshot testing plugin
1 parent 426810f commit 73647fb

14 files changed

Lines changed: 925 additions & 23 deletions

README.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ testImplementation 'io.github.origin-energy:java-snapshot-testing-plugin-jackson
3636
testImplementation 'com.fasterxml.jackson.core:jackson-core:2.11.3'
3737
testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.11.3'
3838
39+
// For Jackson 3 use the dedicated plugin and serializer classes instead
40+
testImplementation 'io.github.origin-energy:java-snapshot-testing-plugin-jackson3:4.+'
41+
testImplementation 'tools.jackson.core:jackson-core:3.1.0'
42+
testImplementation 'tools.jackson.core:jackson-databind:3.1.0'
43+
3944
// Optional: If you want Jackson to serialize Java 8 date/time types or Optionals you should also add the following dependencies
4045
testRuntimeOnly 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.11.3'
4146
testRuntimeOnly 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.11.3'
@@ -51,6 +56,10 @@ serializer=au.com.origin.snapshots.serializers.v1.ToStringSnapshotSerializer
5156
serializer.base64=au.com.origin.snapshots.serializers.v1.Base64SnapshotSerializer
5257
serializer.json=au.com.origin.snapshots.jackson.serializers.v1.JacksonSnapshotSerializer
5358
serializer.orderedJson=au.com.origin.snapshots.jackson.serializers.v1.DeterministicJacksonSnapshotSerializer
59+
60+
# Jackson 3 alternative
61+
# serializer.json=au.com.origin.snapshots.jackson3.serializers.v1.Jackson3SnapshotSerializer
62+
# serializer.orderedJson=au.com.origin.snapshots.jackson3.serializers.v1.DeterministicJackson3SnapshotSerializer
5463
comparator=au.com.origin.snapshots.comparators.v1.PlainTextEqualsComparator
5564
reporters=au.com.origin.snapshots.reporters.v1.PlainTextSnapshotReporter
5665
snapshot-dir=__snapshots__
@@ -151,14 +160,18 @@ We currently support:
151160
Plugins
152161

153162
- [Jackson for JSON serialization](https://search.maven.org/search?q=a:java-snapshot-testing-plugin-jackson)
163+
- [Jackson 3 for JSON serialization](https://search.maven.org/search?q=a:java-snapshot-testing-plugin-jackson3)
154164
- You need jackson on your classpath (Gradle example)
155165
```groovy
156-
// Required java-snapshot-testing peer dependencies
166+
// Jackson 2 plugin
167+
testImplementation 'io.github.origin-energy:java-snapshot-testing-plugin-jackson:4.+'
157168
testImplementation 'com.fasterxml.jackson.core:jackson-core:2.11.3'
158169
testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.11.3'
159-
// Optional java-snapshot-testing peer dependencies
160-
testRuntimeOnly 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.11.3'
161-
testRuntimeOnly 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.11.3'
170+
171+
// Jackson 3 plugin
172+
testImplementation 'io.github.origin-energy:java-snapshot-testing-plugin-jackson3:4.+'
173+
testImplementation 'tools.jackson.core:jackson-core:3.1.0'
174+
testImplementation 'tools.jackson.core:jackson-databind:3.1.0'
162175
```
163176
164177
## How does it work?

build.gradle

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,34 +32,56 @@ subprojects { subproject ->
3232
mavenCentral()
3333
}
3434

35-
tasks.withType(JavaCompile).configureEach {
36-
javaCompiler = javaToolchains.compilerFor {
37-
languageVersion = JavaLanguageVersion.of(11)
35+
if (subproject.name == 'java-snapshot-testing-plugin-jackson3') {
36+
tasks.withType(JavaCompile).configureEach {
37+
javaCompiler = javaToolchains.compilerFor {
38+
languageVersion = JavaLanguageVersion.of(17)
39+
}
3840
}
39-
}
4041

41-
tasks.withType(Javadoc).configureEach {
42-
javadocTool = javaToolchains.javadocToolFor {
43-
languageVersion = JavaLanguageVersion.of(11)
42+
tasks.withType(Javadoc).configureEach {
43+
javadocTool = javaToolchains.javadocToolFor {
44+
languageVersion = JavaLanguageVersion.of(17)
45+
}
4446
}
45-
}
4647

47-
tasks.withType(Test).configureEach {
48-
javaLauncher = javaToolchains.launcherFor {
49-
languageVersion = JavaLanguageVersion.of(11)
48+
tasks.withType(Test).configureEach {
49+
javaLauncher = javaToolchains.launcherFor {
50+
languageVersion = JavaLanguageVersion.of(17)
51+
}
52+
}
53+
} else {
54+
tasks.withType(JavaCompile).configureEach {
55+
javaCompiler = javaToolchains.compilerFor {
56+
languageVersion = JavaLanguageVersion.of(11)
57+
}
58+
}
59+
60+
tasks.withType(Javadoc).configureEach {
61+
javadocTool = javaToolchains.javadocToolFor {
62+
languageVersion = JavaLanguageVersion.of(11)
63+
}
64+
}
65+
66+
tasks.withType(Test).configureEach {
67+
javaLauncher = javaToolchains.launcherFor {
68+
languageVersion = JavaLanguageVersion.of(11)
69+
}
5070
}
51-
}
5271

53-
tasks.withType(GroovyCompile).configureEach {
54-
javaLauncher = javaToolchains.launcherFor {
55-
languageVersion = JavaLanguageVersion.of(11)
72+
tasks.withType(GroovyCompile).configureEach {
73+
javaLauncher = javaToolchains.launcherFor {
74+
languageVersion = JavaLanguageVersion.of(11)
75+
}
5676
}
5777
}
5878

5979
shadowJar {
6080
archiveClassifier = ''
61-
relocate 'org.assertj', 'shadow.org.assertj'
62-
relocate 'org.opentest4j', 'shadow.org.opentest4j'
81+
if (subproject.name != 'java-snapshot-testing-plugin-jackson3') {
82+
relocate 'org.assertj', 'shadow.org.assertj'
83+
relocate 'org.opentest4j', 'shadow.org.opentest4j'
84+
}
6385
exclude 'module-info.class'
6486
}
6587

@@ -82,4 +104,4 @@ subprojects { subproject ->
82104
exceptionFormat "full"
83105
}
84106
}
85-
}
107+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import org.gradle.api.tasks.compile.JavaCompile
2+
import org.gradle.api.tasks.javadoc.Javadoc
3+
import org.gradle.api.tasks.testing.Test
4+
5+
apply from: "../gradle/publishing.gradle"
6+
apply from: "../gradle/spotless.gradle"
7+
apply from: "../gradle/junit5.gradle"
8+
9+
java {
10+
sourceCompatibility = JavaVersion.VERSION_17
11+
targetCompatibility = JavaVersion.VERSION_17
12+
}
13+
14+
tasks.withType(JavaCompile).configureEach {
15+
javaCompiler = javaToolchains.compilerFor {
16+
languageVersion = JavaLanguageVersion.of(17)
17+
}
18+
}
19+
20+
tasks.withType(Javadoc).configureEach {
21+
javadocTool = javaToolchains.javadocToolFor {
22+
languageVersion = JavaLanguageVersion.of(17)
23+
}
24+
}
25+
26+
tasks.withType(Test).configureEach {
27+
javaLauncher = javaToolchains.launcherFor {
28+
languageVersion = JavaLanguageVersion.of(17)
29+
}
30+
}
31+
32+
dependencies {
33+
compileOnly project(':java-snapshot-testing-core')
34+
35+
compileOnly 'tools.jackson.core:jackson-core:3.1.0'
36+
compileOnly 'tools.jackson.core:jackson-databind:3.1.0'
37+
38+
testImplementation project(':java-snapshot-testing-core')
39+
testImplementation "org.slf4j:slf4j-simple:${project.slf4jVersion}"
40+
testImplementation "org.assertj:assertj-core:${project.assertjVersion}"
41+
42+
testImplementation 'tools.jackson.core:jackson-core:3.1.0'
43+
testImplementation 'tools.jackson.core:jackson-databind:3.1.0'
44+
}
45+
46+
publishing {
47+
publications {
48+
myPublication(MavenPublication) {
49+
artifact shadowJar
50+
groupId 'io.github.origin-energy'
51+
artifactId 'java-snapshot-testing-plugin-jackson3'
52+
}
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package au.com.origin.snapshots.jackson3.serializers;
2+
3+
import java.util.Collection;
4+
import java.util.Collections;
5+
import java.util.Objects;
6+
import java.util.stream.Collectors;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
import tools.jackson.core.JsonGenerator;
10+
import tools.jackson.databind.SerializationContext;
11+
import tools.jackson.databind.ValueSerializer;
12+
import tools.jackson.databind.module.SimpleModule;
13+
14+
/**
15+
* Inspired by:
16+
* https://www.stubbornjava.com/posts/creating-a-somewhat-deterministic-jackson-objectmapper
17+
*/
18+
public class DeterministicCollectionModule extends SimpleModule {
19+
20+
private static final Logger LOGGER = LoggerFactory.getLogger(DeterministicCollectionModule.class);
21+
22+
public DeterministicCollectionModule() {
23+
addSerializer(Collection.class, new CollectionSerializer());
24+
}
25+
26+
private static class CollectionSerializer extends ValueSerializer<Collection> {
27+
28+
@Override
29+
public void serialize(Collection value, JsonGenerator gen, SerializationContext context) {
30+
Object[] sorted = convert(value);
31+
context.writeValue(gen, sorted);
32+
}
33+
34+
private Object[] convert(Collection<?> value) {
35+
if (value == null || value.isEmpty()) {
36+
return Collections.emptyList().toArray();
37+
}
38+
39+
try {
40+
return value.stream()
41+
.filter(Objects::nonNull)
42+
.sorted()
43+
.collect(Collectors.toList())
44+
.toArray();
45+
} catch (ClassCastException ex) {
46+
LOGGER.warn(
47+
"Unable to sort() collection - this may result in a non deterministic snapshot.\n"
48+
+ "Consider adding a custom serializer for this type via the Jackson3SnapshotSerializer#configure() method.\n"
49+
+ ex.getMessage());
50+
return value.toArray();
51+
}
52+
}
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package au.com.origin.snapshots.jackson3.serializers.v1;
2+
3+
import au.com.origin.snapshots.jackson3.serializers.DeterministicCollectionModule;
4+
import tools.jackson.databind.MapperFeature;
5+
import tools.jackson.databind.json.JsonMapper;
6+
7+
/**
8+
* Attempts to deterministically render a snapshot.
9+
*
10+
* <p>This can help in situations where collections are rendering in a different order on subsequent
11+
* runs.
12+
*
13+
* <p>Note that collections will be ordered which mar or may not be desirable given your use case.
14+
*/
15+
public class DeterministicJackson3SnapshotSerializer extends Jackson3SnapshotSerializer {
16+
17+
@Override
18+
protected JsonMapper.Builder configure(JsonMapper.Builder builder) {
19+
return super.configure(builder)
20+
.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
21+
.disable(MapperFeature.SORT_CREATOR_PROPERTIES_FIRST)
22+
.addModule(new DeterministicCollectionModule());
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package au.com.origin.snapshots.jackson3.serializers.v1;
2+
3+
import au.com.origin.snapshots.Snapshot;
4+
import au.com.origin.snapshots.SnapshotSerializerContext;
5+
import au.com.origin.snapshots.exceptions.SnapshotExtensionException;
6+
import au.com.origin.snapshots.serializers.SerializerType;
7+
import au.com.origin.snapshots.serializers.SnapshotSerializer;
8+
import com.fasterxml.jackson.annotation.JsonAutoDetect;
9+
import com.fasterxml.jackson.annotation.JsonInclude;
10+
import java.util.Arrays;
11+
import java.util.List;
12+
import tools.jackson.core.PrettyPrinter;
13+
import tools.jackson.databind.MapperFeature;
14+
import tools.jackson.databind.ObjectMapper;
15+
import tools.jackson.databind.SerializationFeature;
16+
import tools.jackson.databind.cfg.DateTimeFeature;
17+
import tools.jackson.databind.json.JsonMapper;
18+
19+
public class Jackson3SnapshotSerializer implements SnapshotSerializer {
20+
21+
private final PrettyPrinter pp = new SnapshotPrettyPrinter();
22+
private final ObjectMapper objectMapper = buildObjectMapper();
23+
24+
protected JsonMapper.Builder configure(JsonMapper.Builder builder) {
25+
return builder;
26+
}
27+
28+
protected boolean shouldFindAndRegisterModules() {
29+
return true;
30+
}
31+
32+
private ObjectMapper buildObjectMapper() {
33+
JsonMapper.Builder builder =
34+
JsonMapper.builder()
35+
.enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS)
36+
.disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)
37+
.enable(DateTimeFeature.WRITE_DATES_WITH_ZONE_ID)
38+
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
39+
.disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
40+
.changeDefaultPropertyInclusion(
41+
inclusion -> inclusion.withValueInclusion(JsonInclude.Include.NON_NULL))
42+
.changeDefaultVisibility(
43+
visibility ->
44+
visibility
45+
.withFieldVisibility(JsonAutoDetect.Visibility.ANY)
46+
.withGetterVisibility(JsonAutoDetect.Visibility.NONE)
47+
.withIsGetterVisibility(JsonAutoDetect.Visibility.NONE)
48+
.withSetterVisibility(JsonAutoDetect.Visibility.NONE)
49+
.withCreatorVisibility(JsonAutoDetect.Visibility.NONE));
50+
51+
if (shouldFindAndRegisterModules()) {
52+
builder.findAndAddModules();
53+
}
54+
return configure(builder).build();
55+
}
56+
57+
@Override
58+
public Snapshot apply(Object object, SnapshotSerializerContext gen) {
59+
try {
60+
List<?> objects = Arrays.asList(object);
61+
String body = objectMapper.writer().with(pp).writeValueAsString(objects);
62+
return gen.toSnapshot(body);
63+
} catch (Exception e) {
64+
throw new SnapshotExtensionException("Jackson Serialization failed", e);
65+
}
66+
}
67+
68+
@Override
69+
public String getOutputFormat() {
70+
return SerializerType.JSON.name();
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package au.com.origin.snapshots.jackson3.serializers.v1;
2+
3+
import tools.jackson.core.JsonGenerator;
4+
import tools.jackson.core.util.DefaultIndenter;
5+
import tools.jackson.core.util.DefaultPrettyPrinter;
6+
import tools.jackson.core.util.Separators;
7+
8+
class SnapshotPrettyPrinter extends DefaultPrettyPrinter {
9+
10+
private static final Separators SNAPSHOT_SEPARATORS =
11+
Separators.createDefaultInstance().withObjectNameValueSpacing(Separators.Spacing.AFTER);
12+
13+
public SnapshotPrettyPrinter() {
14+
this(SNAPSHOT_SEPARATORS);
15+
}
16+
17+
private SnapshotPrettyPrinter(Separators separators) {
18+
super(separators);
19+
Indenter lfOnlyIndenter = new DefaultIndenter(" ", "\n");
20+
this.indentArraysWith(lfOnlyIndenter);
21+
this.indentObjectsWith(lfOnlyIndenter);
22+
}
23+
24+
private SnapshotPrettyPrinter(SnapshotPrettyPrinter base) {
25+
super(base);
26+
}
27+
28+
private SnapshotPrettyPrinter(SnapshotPrettyPrinter base, Separators separators) {
29+
super(base, separators);
30+
}
31+
32+
@Override
33+
public void writeRootValueSeparator(JsonGenerator gen) {}
34+
35+
@Override
36+
public SnapshotPrettyPrinter withSeparators(Separators separators) {
37+
return new SnapshotPrettyPrinter(this, separators);
38+
}
39+
40+
@Override
41+
public DefaultPrettyPrinter createInstance() {
42+
return new SnapshotPrettyPrinter(this);
43+
}
44+
}

0 commit comments

Comments
 (0)