Skip to content
Open
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
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() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ignoringAudition()/ignoreAudition look like a typo — the fields being excluded (createdAt, createdBy, updatedAt, updatedBy) are audit fields, not "audition" fields. Worth renaming to ignoringAuditFields()/ignoreAuditFields.

this.ignoreAudition = true;
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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?


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);
}
}