-
Notifications
You must be signed in to change notification settings - Fork 0
Add response body matchers #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
aacd00c
abee9c3
a77c134
d24fd80
3848bd3
f0c6893
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,98 @@ | ||||||
| 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; | ||||||
|
|
||||||
| 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; | ||||||
|
|
||||||
| @NullMarked | ||||||
| public final class ResponseBodyMatchers { | ||||||
| private static final JsonMapper JSON_MAPPER = 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; | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| return this; | ||||||
| } | ||||||
|
|
||||||
| public <T> ResultMatcher containsObjectAsJson( | ||||||
| Object expectedObject, | ||||||
| Class<T> targetClass | ||||||
| ) { | ||||||
| return mvcResult -> { | ||||||
| try { | ||||||
| var json = mvcResult.getResponse().getContentAsString(); | ||||||
| var extractedValue = JsonPath.read(json, jsonPath); | ||||||
| T actualObject = JSON_MAPPER.readValue(JSON_MAPPER.writeValueAsString(extractedValue), targetClass); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to AI you could use JSON_MAPPER.convertValue(extractedValue, targetClass) instead of serializing to a string and re-parsing — same result, avoids a redundant JSON round-trip. Can you check that proposal?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will check
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tested it in Aichner and EMVA and it works. So I will change it as suggested |
||||||
|
|
||||||
| 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); | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ignoringAudition()/ignoreAuditionlook like a typo — the fields being excluded (createdAt,createdBy,updatedAt,updatedBy) are audit fields, not "audition" fields. Worth renaming toignoringAuditFields()/ignoreAuditFields.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK