From 69c44dee9946d3bb1a1aa0ddef16f3226df6acc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 13 Jun 2024 00:12:33 +0200 Subject: [PATCH] Add support for conversion of the whole JSON document Closes gh-33018 --- .../test/json/AbstractJsonContentAssert.java | 72 +++++++++++++++++++ .../json/AbstractJsonContentAssertTests.java | 67 +++++++++++++++-- 2 files changed, 134 insertions(+), 5 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java index f2992f1418ce..75f4feb25499 100644 --- a/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java +++ b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java @@ -18,15 +18,20 @@ import java.io.File; import java.io.InputStream; +import java.lang.reflect.Type; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.function.Consumer; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.PathNotFoundException; +import org.assertj.core.api.AbstractAssert; import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.AssertFactory; import org.assertj.core.api.AssertProvider; import org.assertj.core.api.Assertions; +import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.error.BasicErrorMessageFactory; import org.assertj.core.internal.Failures; @@ -35,8 +40,12 @@ import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.MediaType; import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.lang.Nullable; +import org.springframework.mock.http.MockHttpInputMessage; import org.springframework.util.Assert; /** @@ -90,6 +99,62 @@ protected AbstractJsonContentAssert(@Nullable JsonContent actual, Class selfT as("JSON content"); } + /** + * Verify that the actual value can be converted to an instance of the + * given {@code target}, and produce a new {@linkplain AbstractObjectAssert + * assertion} object narrowed to that type. + * @param target the {@linkplain Class type} to convert the actual value to + */ + public AbstractObjectAssert convertTo(Class target) { + isNotNull(); + T value = convertToTargetType(target); + return Assertions.assertThat(value); + } + + /** + * Verify that the actual value can be converted to an instance of the type + * defined by the given {@link AssertFactory} and return a new Assert narrowed + * to that type. + *

{@link InstanceOfAssertFactories} provides static factories for all the + * types supported by {@link Assertions#assertThat}. Additional factories can + * be created by implementing {@link AssertFactory}. + *

Example:


+	 * // Check that the JSON document is an array of 3 users
+	 * assertThat(json).convertTo(InstanceOfAssertFactories.list(User.class))
+	 *         hasSize(3); // ListAssert of User
+	 * 
+ * @param assertFactory the {@link AssertFactory} to use to produce a narrowed + * Assert for the type that it defines. + */ + public > ASSERT convertTo(AssertFactory assertFactory) { + isNotNull(); + return assertFactory.createAssert(this::convertToTargetType); + } + + @SuppressWarnings("unchecked") + private T convertToTargetType(Type targetType) { + String json = this.actual.getJson(); + if (this.jsonMessageConverter == null) { + throw new IllegalStateException( + "No JSON message converter available to convert %s".formatted(json)); + } + try { + return (T) this.jsonMessageConverter.read(targetType, getClass(), fromJson(json)); + } + catch (Exception ex) { + throw failure(new ValueProcessingFailed(json, + "To convert successfully to:%n %s%nBut it failed:%n %s%n".formatted( + targetType.getTypeName(), ex.getMessage()))); + } + } + + private HttpInputMessage fromJson(String json) { + MockHttpInputMessage inputMessage = new MockHttpInputMessage(json.getBytes(StandardCharsets.UTF_8)); + inputMessage.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + return inputMessage; + } + + // JsonPath support /** @@ -525,4 +590,11 @@ private JsonPathNotExpected(String actual, String path) { } } + private static final class ValueProcessingFailed extends BasicErrorMessageFactory { + + private ValueProcessingFailed(String actualToString, String errorMessage) { + super("%nExpected:%n %s%n%s".formatted(actualToString, errorMessage)); + } + } + } diff --git a/spring-test/src/test/java/org/springframework/test/json/AbstractJsonContentAssertTests.java b/spring-test/src/test/java/org/springframework/test/json/AbstractJsonContentAssertTests.java index 80ebee1c568d..29caa39a074d 100644 --- a/spring-test/src/test/java/org/springframework/test/json/AbstractJsonContentAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/json/AbstractJsonContentAssertTests.java @@ -26,9 +26,12 @@ import java.util.function.Consumer; import java.util.stream.Stream; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.ObjectMapper; +import org.assertj.core.api.AbstractObjectAssert; import org.assertj.core.api.AssertProvider; import org.assertj.core.api.InstanceOfAssertFactories; +import org.assertj.core.api.InstanceOfAssertFactory; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.Nested; @@ -100,6 +103,56 @@ void satisfiesAllowFurtherAssertions() { }); } + @Nested + class ConversionTests { + + @Test + void convertToTargetType() { + assertThat(forJson(SIMPSONS, jsonHttpMessageConverter)) + .convertTo(Family.class) + .satisfies(family -> assertThat(family.familyMembers()).hasSize(5)); + } + + @Test + void convertToIncompatibleTargetTypeShouldFail() { + AbstractJsonContentAssert jsonAssert = assertThat(forJson(SIMPSONS, jsonHttpMessageConverter)); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> jsonAssert.convertTo(Member.class)) + .withMessageContainingAll("To convert successfully to:", + Member.class.getName(), "But it failed:"); + } + + @Test + void convertUsingAssertFactory() { + assertThat(forJson(SIMPSONS, jsonHttpMessageConverter)) + .convertTo(new FamilyAssertFactory()) + .hasFamilyMember("Homer"); + } + + private AssertProvider> forJson(@Nullable String json, + @Nullable GenericHttpMessageConverter jsonHttpMessageConverter) { + + return () -> new TestJsonContentAssert(json, jsonHttpMessageConverter); + } + + private static class FamilyAssertFactory extends InstanceOfAssertFactory { + public FamilyAssertFactory() { + super(Family.class, FamilyAssert::new); + } + } + + private static class FamilyAssert extends AbstractObjectAssert { + public FamilyAssert(Family family) { + super(family, FamilyAssert.class); + } + + public FamilyAssert hasFamilyMember(String name) { + assertThat(this.actual.familyMembers).anySatisfy(m -> assertThat(m.name()).isEqualTo(name)); + return this.myself; + } + } + } + @Nested class HasPathTests { @@ -261,14 +314,14 @@ void asMapIsEmpty() { void convertToWithoutHttpMessageConverterShouldFail() { JsonPathValueAssert path = assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[0]"); assertThatIllegalStateException() - .isThrownBy(() -> path.convertTo(ExtractingPathTests.Member.class)) + .isThrownBy(() -> path.convertTo(Member.class)) .withMessage("No JSON message converter available to convert {name=Homer}"); } @Test void convertToTargetType() { assertThat(forJson(SIMPSONS, jsonHttpMessageConverter)) - .extractingPath("$.familyMembers[0]").convertTo(ExtractingPathTests.Member.class) + .extractingPath("$.familyMembers[0]").convertTo(Member.class) .satisfies(member -> assertThat(member.name).isEqualTo("Homer")); } @@ -283,7 +336,7 @@ void convertToIncompatibleTargetTypeShouldFail() { } @Test - void convertArrayToParameterizedType() { + void convertArrayUsingAssertFactory() { assertThat(forJson(SIMPSONS, jsonHttpMessageConverter)) .extractingPath("$.familyMembers") .convertTo(InstanceOfAssertFactories.list(Member.class)) @@ -336,8 +389,6 @@ void isNotEmptyForPathWithFilterNotMatching() { } - private record Member(String name) {} - private record Customer(long id, String username) {} private AssertProvider> forJson(@Nullable String json) { @@ -836,6 +887,12 @@ private AssertProvider> forJson(@Nullable String js return () -> new TestJsonContentAssert(json, null); } + + record Member(String name) {} + + @JsonIgnoreProperties(ignoreUnknown = true) + record Family(List familyMembers) {} + private static class TestJsonContentAssert extends AbstractJsonContentAssert { public TestJsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter jsonMessageConverter) {