From aacd00c6cf0d2347e35a5e1665afac484ca3ffc1 Mon Sep 17 00:00:00 2001 From: Peter Moser Date: Wed, 15 Jul 2026 09:54:55 +0200 Subject: [PATCH 1/4] add ResponseBodyMatchers used in EMVA and AICHNER --- .../web/response/ResponseBodyMatchers.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/main/java/it/aboutbits/springboot/testing/web/response/ResponseBodyMatchers.java diff --git a/src/main/java/it/aboutbits/springboot/testing/web/response/ResponseBodyMatchers.java b/src/main/java/it/aboutbits/springboot/testing/web/response/ResponseBodyMatchers.java new file mode 100644 index 0000000..248d9b5 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/web/response/ResponseBodyMatchers.java @@ -0,0 +1,95 @@ +package it.aboutbits.springboot.testing.web.response; + +import com.jayway.jsonpath.JsonPath; +import org.springframework.test.web.servlet.ResultMatcher; +import tools.jackson.databind.json.JsonMapper; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.fail; + +public final class ResponseBodyMatchers { + public static JsonMapper jsonMapper = new JsonMapper(); + private final String jsonPath; + private String[] fieldNamesToIgnore = new String[0]; + private String[] optionalFieldNamesToIgnore = new String[0]; + private boolean ignoreAudition = false; + + private ResponseBodyMatchers(String jsonPath) { + this.jsonPath = jsonPath; + } + + public ResponseBodyMatchers ignoringOptionalFields(String... optionalFieldNamesToIgnore) { + this.optionalFieldNamesToIgnore = optionalFieldNamesToIgnore; + return this; + } + + public ResponseBodyMatchers ignoringFields(String... fieldNamesToIgnore) { + this.fieldNamesToIgnore = fieldNamesToIgnore; + return this; + } + + public ResponseBodyMatchers ignoringAudition() { + this.ignoreAudition = true; + return this; + } + + public ResultMatcher containsObjectAsJson( + Object expectedObject, + Class targetClass + ) { + return mvcResult -> { + try { + var json = mvcResult.getResponse().getContentAsString(); + var extractedValue = JsonPath.read(json, jsonPath); + T actualObject = jsonMapper.readValue(jsonMapper.writeValueAsString(extractedValue), targetClass); + + var ignoredFields = new ArrayList<>(Arrays.asList(fieldNamesToIgnore)); + ignoredFields.addAll(Arrays.asList(optionalFieldNamesToIgnore)); + if (ignoreAudition) { + ignoredFields.addAll(Arrays.asList("createdAt", "createdBy", "updatedAt", "updatedBy")); + } + + assertThat(actualObject) + .usingComparatorForType(BigDecimal::compareTo, BigDecimal.class) + .usingComparatorForType(ResponseBodyMatchers::compareTemporalByDelta, OffsetDateTime.class) + .usingComparatorForType(ResponseBodyMatchers::compareTemporalByDelta, LocalDateTime.class) + .usingRecursiveComparison() + .ignoringFields(ignoredFields.toArray(new String[0])) + .isEqualTo(expectedObject); + + for (var requiredFieldName : fieldNamesToIgnore) { + assertThatCode( + () -> JsonPath.read(json, jsonPath + "." + requiredFieldName) + ).doesNotThrowAnyException(); + } + } catch (Exception e) { + fail("Unable to extract result set. Maybe the path does not contain an object of that type?", e); + } + }; + } + + // Not every machine has 9 digits precision, so we need to compare with a delta + private static int compareTemporalByDelta(OffsetDateTime a, OffsetDateTime b) { + return Math.abs(ChronoUnit.NANOS.between(a, b)) < 1000 ? 0 : a.compareTo(b); + } + + private static int compareTemporalByDelta(LocalDateTime a, LocalDateTime b) { + return Math.abs(ChronoUnit.NANOS.between(a, b)) < 1000 ? 0 : a.compareTo(b); + } + + public static ResponseBodyMatchers responseBody() { + return new ResponseBodyMatchers("$"); + } + + public static ResponseBodyMatchers responseBodyAt(String expression) { + return new ResponseBodyMatchers(expression); + } +} From abee9c3c6f24b998053f59308c1cbd1f14bd3b1e Mon Sep 17 00:00:00 2001 From: Peter Moser Date: Wed, 15 Jul 2026 09:58:45 +0200 Subject: [PATCH 2/4] make json mapper private --- .../springboot/testing/web/response/ResponseBodyMatchers.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/it/aboutbits/springboot/testing/web/response/ResponseBodyMatchers.java b/src/main/java/it/aboutbits/springboot/testing/web/response/ResponseBodyMatchers.java index 248d9b5..8cde8d6 100644 --- a/src/main/java/it/aboutbits/springboot/testing/web/response/ResponseBodyMatchers.java +++ b/src/main/java/it/aboutbits/springboot/testing/web/response/ResponseBodyMatchers.java @@ -16,8 +16,9 @@ import static org.assertj.core.api.Assertions.fail; public final class ResponseBodyMatchers { - public static JsonMapper jsonMapper = new JsonMapper(); + private final static JsonMapper jsonMapper = new JsonMapper(); private final String jsonPath; + private String[] fieldNamesToIgnore = new String[0]; private String[] optionalFieldNamesToIgnore = new String[0]; private boolean ignoreAudition = false; From a77c134456531fcb2dbffb6ad409dcb040ff6f7b Mon Sep 17 00:00:00 2001 From: Peter Moser Date: Wed, 15 Jul 2026 10:00:32 +0200 Subject: [PATCH 3/4] fix reviewed issues --- .../springboot/testing/web/response/ResponseBodyMatchers.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/it/aboutbits/springboot/testing/web/response/ResponseBodyMatchers.java b/src/main/java/it/aboutbits/springboot/testing/web/response/ResponseBodyMatchers.java index 8cde8d6..691a997 100644 --- a/src/main/java/it/aboutbits/springboot/testing/web/response/ResponseBodyMatchers.java +++ b/src/main/java/it/aboutbits/springboot/testing/web/response/ResponseBodyMatchers.java @@ -16,7 +16,7 @@ import static org.assertj.core.api.Assertions.fail; public final class ResponseBodyMatchers { - private final static JsonMapper jsonMapper = new JsonMapper(); + private static final JsonMapper jsonMapper = new JsonMapper(); private final String jsonPath; private String[] fieldNamesToIgnore = new String[0]; From d24fd8059052566e7274d5a2ead979f2bb7778b4 Mon Sep 17 00:00:00 2001 From: Peter Moser Date: Wed, 15 Jul 2026 10:04:18 +0200 Subject: [PATCH 4/4] fix style violations --- .../testing/web/response/ResponseBodyMatchers.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/it/aboutbits/springboot/testing/web/response/ResponseBodyMatchers.java b/src/main/java/it/aboutbits/springboot/testing/web/response/ResponseBodyMatchers.java index 691a997..e4d9b97 100644 --- a/src/main/java/it/aboutbits/springboot/testing/web/response/ResponseBodyMatchers.java +++ b/src/main/java/it/aboutbits/springboot/testing/web/response/ResponseBodyMatchers.java @@ -1,6 +1,7 @@ package it.aboutbits.springboot.testing.web.response; import com.jayway.jsonpath.JsonPath; +import org.jspecify.annotations.NullMarked; import org.springframework.test.web.servlet.ResultMatcher; import tools.jackson.databind.json.JsonMapper; @@ -15,8 +16,9 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.fail; +@NullMarked public final class ResponseBodyMatchers { - private static final JsonMapper jsonMapper = new JsonMapper(); + private static final JsonMapper JSON_MAPPER = new JsonMapper(); private final String jsonPath; private String[] fieldNamesToIgnore = new String[0]; @@ -50,7 +52,7 @@ public ResultMatcher containsObjectAsJson( try { var json = mvcResult.getResponse().getContentAsString(); var extractedValue = JsonPath.read(json, jsonPath); - T actualObject = jsonMapper.readValue(jsonMapper.writeValueAsString(extractedValue), targetClass); + T actualObject = JSON_MAPPER.readValue(JSON_MAPPER.writeValueAsString(extractedValue), targetClass); var ignoredFields = new ArrayList<>(Arrays.asList(fieldNamesToIgnore)); ignoredFields.addAll(Arrays.asList(optionalFieldNamesToIgnore));