From e2c5887cea4c41508703134c312d8b20ca6c3728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 27 May 2024 09:25:12 +0200 Subject: [PATCH] Allow JSON content assertions to be nested Previously, AbstractJsonContentAssert worked on a raw String, which made standard AssertJ nested calls, such as satisfies, to return an assert on the raw string, rather than one with JSON support. This commit rework AbstractJsonContentAssert so that it no longer extend from AbstractStringAssert. This makes the list of methods more focused on JSON assertions, and allow standard operations to provide the right assert object. Closes gh-32894 --- .../test/json/AbstractJsonContentAssert.java | 63 ++++++++++++++----- .../test/json/JsonContent.java | 31 ++++++--- .../test/json/JsonContentAssert.java | 8 +-- ...AbstractMockHttpServletResponseAssert.java | 3 +- .../json/AbstractJsonContentAssertTests.java | 10 ++- .../test/json/JsonContentTests.java | 20 ++++-- 6 files changed, 100 insertions(+), 35 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 1f5cda8aa4e6..f2992f1418ce 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 @@ -24,8 +24,9 @@ import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.PathNotFoundException; -import org.assertj.core.api.AbstractStringAssert; +import org.assertj.core.api.AbstractObjectAssert; import org.assertj.core.api.AssertProvider; +import org.assertj.core.api.Assertions; import org.assertj.core.error.BasicErrorMessageFactory; import org.assertj.core.internal.Failures; @@ -61,7 +62,7 @@ * @param the type of assertions */ public abstract class AbstractJsonContentAssert> - extends AbstractStringAssert { + extends AbstractObjectAssert { private static final Failures failures = Failures.instance(); @@ -79,16 +80,12 @@ public abstract class AbstractJsonContentAssertPath can be converted to a value object using the given - * {@linkplain GenericHttpMessageConverter JSON message converter}. - * @param json the JSON document to assert - * @param jsonMessageConverter the converter to use + * @param actual the JSON document to assert * @param selfType the implementation type of this assert */ - protected AbstractJsonContentAssert(@Nullable String json, - @Nullable GenericHttpMessageConverter jsonMessageConverter, Class selfType) { - super(json, selfType); - this.jsonMessageConverter = jsonMessageConverter; + protected AbstractJsonContentAssert(@Nullable JsonContent actual, Class selfType) { + super(actual, selfType); + this.jsonMessageConverter = (actual != null ? actual.getJsonMessageConverter() : null); this.jsonLoader = new JsonLoader(null, null); as("JSON content"); } @@ -141,6 +138,19 @@ public SELF doesNotHavePath(String path) { // JsonAssert support + /** + * Verify that the actual value is {@linkplain JsonCompareMode#STRICT strictly} + * equal to the given JSON. The {@code expected} value can contain the JSON + * itself or, if it ends with {@code .json}, the name of a resource to be + * loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + * @see #isEqualTo(CharSequence, JsonCompareMode) + */ + public SELF isEqualTo(@Nullable CharSequence expected) { + return isEqualTo(expected, JsonCompareMode.STRICT); + } + /** * Verify that the actual value is equal to the given JSON. The * {@code expected} value can contain the JSON itself or, if it ends with @@ -257,6 +267,19 @@ public SELF isStrictlyEqualTo(Resource expected) { return isEqualTo(expected, JsonCompareMode.STRICT); } + /** + * Verify that the actual value is {@linkplain JsonCompareMode#STRICT strictly} + * not equal to the given JSON. The {@code expected} value can contain the + * JSON itself or, if it ends with {@code .json}, the name of a resource to + * be loaded from the classpath. + * @param expected the expected JSON or the name of a resource containing + * the expected JSON + * @see #isNotEqualTo(CharSequence, JsonCompareMode) + */ + public SELF isNotEqualTo(@Nullable CharSequence expected) { + return isNotEqualTo(expected, JsonCompareMode.STRICT); + } + /** * Verify that the actual value is not equal to the given JSON. The * {@code expected} value can contain the JSON itself or, if it ends with @@ -399,13 +422,24 @@ public SELF withCharset(@Nullable Charset charset) { return this.myself; } + @Nullable + private String toJsonString() { + return (this.actual != null ? this.actual.getJson() : null); + } + + @SuppressWarnings("NullAway") + private String toNonNullJsonString() { + String jsonString = toJsonString(); + Assertions.assertThat(jsonString).as("JSON content").isNotNull(); + return jsonString; + } private JsonComparison compare(@Nullable CharSequence expectedJson, JsonCompareMode compareMode) { return compare(expectedJson, JsonAssert.comparator(compareMode)); } private JsonComparison compare(@Nullable CharSequence expectedJson, JsonComparator comparator) { - return comparator.compare((expectedJson != null) ? expectedJson.toString() : null, this.actual); + return comparator.compare((expectedJson != null) ? expectedJson.toString() : null, toJsonString()); } private SELF assertIsMatch(JsonComparison result) { @@ -435,16 +469,15 @@ private class JsonPathValue { private final String path; - private final JsonPath jsonPath; - private final String json; + private final JsonPath jsonPath; + JsonPathValue(String path) { Assert.hasText(path, "'path' must not be null or empty"); - isNotNull(); this.path = path; + this.json = toNonNullJsonString(); this.jsonPath = JsonPath.compile(this.path); - this.json = AbstractJsonContentAssert.this.actual; } @Nullable diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonContent.java b/spring-test/src/main/java/org/springframework/test/json/JsonContent.java index c801a5fa5d64..383b2c3c3d24 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonContent.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonContent.java @@ -18,6 +18,7 @@ import org.assertj.core.api.AssertProvider; +import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -34,38 +35,54 @@ public final class JsonContent implements AssertProvider { private final String json; @Nullable - private final Class resourceLoadClass; + private final GenericHttpMessageConverter jsonMessageConverter; /** - * Create a new {@code JsonContent} instance. + * Create a new {@code JsonContent} instance with the message converter to + * use to deserialize content. * @param json the actual JSON content - * @param resourceLoadClass the source class used to load resources + * @param jsonMessageConverter the message converter to use */ - JsonContent(String json, @Nullable Class resourceLoadClass) { + public JsonContent(String json, @Nullable GenericHttpMessageConverter jsonMessageConverter) { Assert.notNull(json, "JSON must not be null"); this.json = json; - this.resourceLoadClass = resourceLoadClass; + this.jsonMessageConverter = jsonMessageConverter; } + /** + * Create a new {@code JsonContent} instance. + * @param json the actual JSON content + */ + public JsonContent(String json) { + this(json, null); + } + /** * Use AssertJ's {@link org.assertj.core.api.Assertions#assertThat assertThat} * instead. */ @Override public JsonContentAssert assertThat() { - return new JsonContentAssert(this.json, null).withResourceLoadClass(this.resourceLoadClass); + return new JsonContentAssert(this); } /** * Return the actual JSON content string. - * @return the JSON content */ public String getJson() { return this.json; } + /** + * Return the message converter to use to deserialize content. + */ + @Nullable + GenericHttpMessageConverter getJsonMessageConverter() { + return this.jsonMessageConverter; + } + @Override public String toString() { return "JsonContent " + this.json; diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java b/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java index de4a35f80f96..9728a762496a 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java @@ -16,7 +16,6 @@ package org.springframework.test.json; -import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.lang.Nullable; /** @@ -29,13 +28,10 @@ public class JsonContentAssert extends AbstractJsonContentAssertPath can be converted to a value object using the given - * {@linkplain GenericHttpMessageConverter JSON message converter}. * @param json the JSON document to assert - * @param jsonMessageConverter the converter to use */ - public JsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter jsonMessageConverter) { - super(json, jsonMessageConverter, JsonContentAssert.class); + public JsonContentAssert(@Nullable JsonContent json) { + super(json, JsonContentAssert.class); } } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java index 9800b2865db9..1bbfd051d94b 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java @@ -27,6 +27,7 @@ import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.json.AbstractJsonContentAssert; +import org.springframework.test.json.JsonContent; import org.springframework.test.json.JsonContentAssert; import org.springframework.test.web.UriAssert; @@ -92,7 +93,7 @@ public AbstractStringAssert bodyText() { * */ public AbstractJsonContentAssert bodyJson() { - return new JsonContentAssert(readBody(), this.jsonMessageConverter); + return new JsonContentAssert(new JsonContent(readBody(), this.jsonMessageConverter)); } private String readBody() { 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 37568a6d5850..0c350c41357c 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 @@ -92,6 +92,14 @@ void isNullWhenActualIsNullShouldPass() { assertThat(forJson(null)).isNull(); } + @Test + void satisfiesAllowFurtherAssertions() { + assertThat(forJson(SIMPSONS)).satisfies(content -> { + assertThat(content).extractingPath("$.familyMembers[0].name").isEqualTo("Homer"); + assertThat(content).extractingPath("$.familyMembers[1].name").isEqualTo("Marge"); + }); + } + @Nested class HasPathTests { @@ -831,7 +839,7 @@ private AssertProvider> forJson(@Nullable String js private static class TestJsonContentAssert extends AbstractJsonContentAssert { public TestJsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter jsonMessageConverter) { - super(json, jsonMessageConverter, TestJsonContentAssert.class); + super((json != null ? new JsonContent(json, jsonMessageConverter) : null), TestJsonContentAssert.class); } } diff --git a/spring-test/src/test/java/org/springframework/test/json/JsonContentTests.java b/spring-test/src/test/java/org/springframework/test/json/JsonContentTests.java index 6e4131c46f66..b87e8849b054 100644 --- a/spring-test/src/test/java/org/springframework/test/json/JsonContentTests.java +++ b/spring-test/src/test/java/org/springframework/test/json/JsonContentTests.java @@ -18,13 +18,17 @@ import org.junit.jupiter.api.Test; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; /** * Tests for {@link JsonContent}. * * @author Phillip Webb + * @author Stephane Nicoll */ class JsonContentTests { @@ -34,27 +38,33 @@ class JsonContentTests { void createWhenJsonIsNullShouldThrowException() { assertThatIllegalArgumentException() .isThrownBy( - () -> new JsonContent(null, null)) + () -> new JsonContent(null)) .withMessageContaining("JSON must not be null"); } @Test - @SuppressWarnings("deprecation") void assertThatShouldReturnJsonContentAssert() { - JsonContent content = new JsonContent(JSON, getClass()); + JsonContent content = new JsonContent(JSON); assertThat(content.assertThat()).isInstanceOf(JsonContentAssert.class); } @Test void getJsonShouldReturnJson() { - JsonContent content = new JsonContent(JSON, getClass()); + JsonContent content = new JsonContent(JSON); assertThat(content.getJson()).isEqualTo(JSON); } @Test void toStringShouldReturnString() { - JsonContent content = new JsonContent(JSON, getClass()); + JsonContent content = new JsonContent(JSON); assertThat(content.toString()).isEqualTo("JsonContent " + JSON); } + @Test + void getJsonMessageConverterShouldReturnConverter() { + MappingJackson2HttpMessageConverter converter = mock(MappingJackson2HttpMessageConverter.class); + JsonContent content = new JsonContent(JSON, converter); + assertThat(content.getJsonMessageConverter()).isSameAs(converter); + } + }