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
This commit is contained in:
Stéphane Nicoll 2024-05-27 09:25:12 +02:00
parent 489d18a169
commit e2c5887cea
6 changed files with 102 additions and 37 deletions

View File

@ -24,8 +24,9 @@ import java.util.function.Consumer;
import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException; 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.AssertProvider;
import org.assertj.core.api.Assertions;
import org.assertj.core.error.BasicErrorMessageFactory; import org.assertj.core.error.BasicErrorMessageFactory;
import org.assertj.core.internal.Failures; import org.assertj.core.internal.Failures;
@ -61,7 +62,7 @@ import org.springframework.util.Assert;
* @param <SELF> the type of assertions * @param <SELF> the type of assertions
*/ */
public abstract class AbstractJsonContentAssert<SELF extends AbstractJsonContentAssert<SELF>> public abstract class AbstractJsonContentAssert<SELF extends AbstractJsonContentAssert<SELF>>
extends AbstractStringAssert<SELF> { extends AbstractObjectAssert<SELF, JsonContent> {
private static final Failures failures = Failures.instance(); private static final Failures failures = Failures.instance();
@ -79,16 +80,12 @@ public abstract class AbstractJsonContentAssert<SELF extends AbstractJsonContent
/** /**
* Create an assert for the given JSON document. * Create an assert for the given JSON document.
* <p>Path can be converted to a value object using the given * @param actual the JSON document to assert
* {@linkplain GenericHttpMessageConverter JSON message converter}.
* @param json the JSON document to assert
* @param jsonMessageConverter the converter to use
* @param selfType the implementation type of this assert * @param selfType the implementation type of this assert
*/ */
protected AbstractJsonContentAssert(@Nullable String json, protected AbstractJsonContentAssert(@Nullable JsonContent actual, Class<?> selfType) {
@Nullable GenericHttpMessageConverter<Object> jsonMessageConverter, Class<?> selfType) { super(actual, selfType);
super(json, selfType); this.jsonMessageConverter = (actual != null ? actual.getJsonMessageConverter() : null);
this.jsonMessageConverter = jsonMessageConverter;
this.jsonLoader = new JsonLoader(null, null); this.jsonLoader = new JsonLoader(null, null);
as("JSON content"); as("JSON content");
} }
@ -141,6 +138,19 @@ public abstract class AbstractJsonContentAssert<SELF extends AbstractJsonContent
// JsonAssert support // 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 * 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 * {@code expected} value can contain the JSON itself or, if it ends with
@ -257,6 +267,19 @@ public abstract class AbstractJsonContentAssert<SELF extends AbstractJsonContent
return isEqualTo(expected, JsonCompareMode.STRICT); 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 * 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 * {@code expected} value can contain the JSON itself or, if it ends with
@ -399,13 +422,24 @@ public abstract class AbstractJsonContentAssert<SELF extends AbstractJsonContent
return this.myself; 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) { private JsonComparison compare(@Nullable CharSequence expectedJson, JsonCompareMode compareMode) {
return compare(expectedJson, JsonAssert.comparator(compareMode)); return compare(expectedJson, JsonAssert.comparator(compareMode));
} }
private JsonComparison compare(@Nullable CharSequence expectedJson, JsonComparator comparator) { 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) { private SELF assertIsMatch(JsonComparison result) {
@ -435,16 +469,15 @@ public abstract class AbstractJsonContentAssert<SELF extends AbstractJsonContent
private final String path; private final String path;
private final JsonPath jsonPath;
private final String json; private final String json;
private final JsonPath jsonPath;
JsonPathValue(String path) { JsonPathValue(String path) {
Assert.hasText(path, "'path' must not be null or empty"); Assert.hasText(path, "'path' must not be null or empty");
isNotNull();
this.path = path; this.path = path;
this.json = toNonNullJsonString();
this.jsonPath = JsonPath.compile(this.path); this.jsonPath = JsonPath.compile(this.path);
this.json = AbstractJsonContentAssert.this.actual;
} }
@Nullable @Nullable

View File

@ -18,6 +18,7 @@ package org.springframework.test.json;
import org.assertj.core.api.AssertProvider; import org.assertj.core.api.AssertProvider;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -34,38 +35,54 @@ public final class JsonContent implements AssertProvider<JsonContentAssert> {
private final String json; private final String json;
@Nullable @Nullable
private final Class<?> resourceLoadClass; private final GenericHttpMessageConverter<Object> jsonMessageConverter;
/**
* Create a new {@code JsonContent} instance with the message converter to
* use to deserialize content.
* @param json the actual JSON content
* @param jsonMessageConverter the message converter to use
*/
public JsonContent(String json, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) {
Assert.notNull(json, "JSON must not be null");
this.json = json;
this.jsonMessageConverter = jsonMessageConverter;
}
/** /**
* Create a new {@code JsonContent} instance. * Create a new {@code JsonContent} instance.
* @param json the actual JSON content * @param json the actual JSON content
* @param resourceLoadClass the source class used to load resources
*/ */
JsonContent(String json, @Nullable Class<?> resourceLoadClass) { public JsonContent(String json) {
Assert.notNull(json, "JSON must not be null"); this(json, null);
this.json = json;
this.resourceLoadClass = resourceLoadClass;
} }
/** /**
* Use AssertJ's {@link org.assertj.core.api.Assertions#assertThat assertThat} * Use AssertJ's {@link org.assertj.core.api.Assertions#assertThat assertThat}
* instead. * instead.
*/ */
@Override @Override
public JsonContentAssert assertThat() { public JsonContentAssert assertThat() {
return new JsonContentAssert(this.json, null).withResourceLoadClass(this.resourceLoadClass); return new JsonContentAssert(this);
} }
/** /**
* Return the actual JSON content string. * Return the actual JSON content string.
* @return the JSON content
*/ */
public String getJson() { public String getJson() {
return this.json; return this.json;
} }
/**
* Return the message converter to use to deserialize content.
*/
@Nullable
GenericHttpMessageConverter<Object> getJsonMessageConverter() {
return this.jsonMessageConverter;
}
@Override @Override
public String toString() { public String toString() {
return "JsonContent " + this.json; return "JsonContent " + this.json;

View File

@ -16,7 +16,6 @@
package org.springframework.test.json; package org.springframework.test.json;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
/** /**
@ -29,13 +28,10 @@ public class JsonContentAssert extends AbstractJsonContentAssert<JsonContentAsse
/** /**
* Create an assert for the given JSON document. * Create an assert for the given JSON document.
* <p>Path can be converted to a value object using the given
* {@linkplain GenericHttpMessageConverter JSON message converter}.
* @param json the JSON document to assert * @param json the JSON document to assert
* @param jsonMessageConverter the converter to use
*/ */
public JsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) { public JsonContentAssert(@Nullable JsonContent json) {
super(json, jsonMessageConverter, JsonContentAssert.class); super(json, JsonContentAssert.class);
} }
} }

View File

@ -27,6 +27,7 @@ import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.json.AbstractJsonContentAssert; import org.springframework.test.json.AbstractJsonContentAssert;
import org.springframework.test.json.JsonContent;
import org.springframework.test.json.JsonContentAssert; import org.springframework.test.json.JsonContentAssert;
import org.springframework.test.web.UriAssert; import org.springframework.test.web.UriAssert;
@ -92,7 +93,7 @@ public abstract class AbstractMockHttpServletResponseAssert<SELF extends Abstrac
* </code></pre> * </code></pre>
*/ */
public AbstractJsonContentAssert<?> bodyJson() { public AbstractJsonContentAssert<?> bodyJson() {
return new JsonContentAssert(readBody(), this.jsonMessageConverter); return new JsonContentAssert(new JsonContent(readBody(), this.jsonMessageConverter));
} }
private String readBody() { private String readBody() {

View File

@ -92,6 +92,14 @@ class AbstractJsonContentAssertTests {
assertThat(forJson(null)).isNull(); 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 @Nested
class HasPathTests { class HasPathTests {
@ -831,7 +839,7 @@ class AbstractJsonContentAssertTests {
private static class TestJsonContentAssert extends AbstractJsonContentAssert<TestJsonContentAssert> { private static class TestJsonContentAssert extends AbstractJsonContentAssert<TestJsonContentAssert> {
public TestJsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) { public TestJsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) {
super(json, jsonMessageConverter, TestJsonContentAssert.class); super((json != null ? new JsonContent(json, jsonMessageConverter) : null), TestJsonContentAssert.class);
} }
} }

View File

@ -18,13 +18,17 @@ package org.springframework.test.json;
import org.junit.jupiter.api.Test; 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.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;
/** /**
* Tests for {@link JsonContent}. * Tests for {@link JsonContent}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Stephane Nicoll
*/ */
class JsonContentTests { class JsonContentTests {
@ -34,27 +38,33 @@ class JsonContentTests {
void createWhenJsonIsNullShouldThrowException() { void createWhenJsonIsNullShouldThrowException() {
assertThatIllegalArgumentException() assertThatIllegalArgumentException()
.isThrownBy( .isThrownBy(
() -> new JsonContent(null, null)) () -> new JsonContent(null))
.withMessageContaining("JSON must not be null"); .withMessageContaining("JSON must not be null");
} }
@Test @Test
@SuppressWarnings("deprecation")
void assertThatShouldReturnJsonContentAssert() { void assertThatShouldReturnJsonContentAssert() {
JsonContent content = new JsonContent(JSON, getClass()); JsonContent content = new JsonContent(JSON);
assertThat(content.assertThat()).isInstanceOf(JsonContentAssert.class); assertThat(content.assertThat()).isInstanceOf(JsonContentAssert.class);
} }
@Test @Test
void getJsonShouldReturnJson() { void getJsonShouldReturnJson() {
JsonContent content = new JsonContent(JSON, getClass()); JsonContent content = new JsonContent(JSON);
assertThat(content.getJson()).isEqualTo(JSON); assertThat(content.getJson()).isEqualTo(JSON);
} }
@Test @Test
void toStringShouldReturnString() { void toStringShouldReturnString() {
JsonContent content = new JsonContent(JSON, getClass()); JsonContent content = new JsonContent(JSON);
assertThat(content.toString()).isEqualTo("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);
}
} }