From 24cc77655f02bbe9e2e1549dd00d2fdcae2b1ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 6 May 2024 11:08:02 +0200 Subject: [PATCH 1/4] Merge JSON assertions in a single Assert type This commit merges the JSONCompare and JsonPath support in a single assert object. The rationale for the previous situation was that JsonPath is optional but merging the assertions methods do not make it mandatory as the usage if JsonPath is triggered only if it is actually used. See gh-32712 --- .../test/json/AbstractJsonContentAssert.java | 501 +++++++++++ .../test/json/JsonContent.java | 2 +- .../test/json/JsonContentAssert.java | 345 +------- .../test/json/JsonPathAssert.java | 170 ---- .../servlet/assertj/ResponseBodyAssert.java | 36 +- .../json/AbstractJsonContentAssertTests.java | 781 ++++++++++++++++++ .../test/json/JsonContentAssertTests.java | 479 ----------- .../test/json/JsonPathAssertTests.java | 323 -------- .../assertj/AssertableMockMvcTests.java | 6 +- 9 files changed, 1320 insertions(+), 1323 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java delete mode 100644 spring-test/src/main/java/org/springframework/test/json/JsonPathAssert.java create mode 100644 spring-test/src/test/java/org/springframework/test/json/AbstractJsonContentAssertTests.java delete mode 100644 spring-test/src/test/java/org/springframework/test/json/JsonContentAssertTests.java delete mode 100644 spring-test/src/test/java/org/springframework/test/json/JsonPathAssertTests.java 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 new file mode 100644 index 00000000000..45b7e839209 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java @@ -0,0 +1,501 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.json; + +import java.io.File; +import java.io.InputStream; +import java.nio.charset.Charset; +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.AbstractStringAssert; +import org.assertj.core.api.AssertProvider; +import org.assertj.core.error.BasicErrorMessageFactory; +import org.assertj.core.internal.Failures; +import org.skyscreamer.jsonassert.JSONCompare; +import org.skyscreamer.jsonassert.JSONCompareMode; +import org.skyscreamer.jsonassert.JSONCompareResult; +import org.skyscreamer.jsonassert.comparator.JSONComparator; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.function.ThrowingBiFunction; + +/** + * Base AssertJ {@link org.assertj.core.api.Assert assertions} that can be + * applied to a JSON document. + * + *

Support evaluating {@linkplain JsonPath JSON path} expressions and + * extracting a part of the document for further {@linkplain JsonPathValueAssert + * assertions} on the value. + * + *

Also support comparing the JSON document against a target, using + * {@linkplain JSONCompare JSON Assert}. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @author Andy Wilkinson + * @author Diego Berrueta + * @author Camille Vienot + * @since 6.2 + * @param the type of assertions + */ +public abstract class AbstractJsonContentAssert> + extends AbstractStringAssert { + + private static final Failures failures = Failures.instance(); + + + @Nullable + private final GenericHttpMessageConverter jsonMessageConverter; + + private final JsonLoader jsonLoader; + + /** + * Create an assert for the given JSON document. + *

Path can be converted to a value object using the given + * {@linkplain GenericHttpMessageConverter json message converter}. + *

Resources to match can be loaded relative to the given + * {@code resourceLoadClass}. If not specified, resources must always be + * absolute. A specific {@link Charset} can be provided if {@code UTF-8} is + * not suitable. + * @param json the JSON document to assert + * @param jsonMessageConverter the converter to use + * @param resourceLoadClass the class used to load resources + * @param charset the charset of the JSON resources + * @param selfType the implementation type of this assert + */ + protected AbstractJsonContentAssert(@Nullable String json, + @Nullable GenericHttpMessageConverter jsonMessageConverter, @Nullable Class resourceLoadClass, + @Nullable Charset charset, Class selfType) { + super(json, selfType); + this.jsonMessageConverter = jsonMessageConverter; + this.jsonLoader = new JsonLoader(resourceLoadClass, charset); + as("JSON content"); + } + + // JsonPath support + + /** + * Verify that the given JSON {@code path} is present, and extract the JSON + * value for further {@linkplain JsonPathValueAssert assertions}. + * @param path the {@link JsonPath} expression + * @see #hasPathSatisfying(String, Consumer) + */ + public JsonPathValueAssert extractingPath(String path) { + Object value = new JsonPathValue(path).getValue(); + return new JsonPathValueAssert(value, path, this.jsonMessageConverter); + } + + /** + * Verify that the given JSON {@code path} is present with a JSON value + * satisfying the given {@code valueRequirements}. + * @param path the {@link JsonPath} expression + * @param valueRequirements a {@link Consumer} of the assertion object + */ + public SELF hasPathSatisfying(String path, Consumer> valueRequirements) { + Object value = new JsonPathValue(path).assertHasPath(); + JsonPathValueAssert valueAssert = new JsonPathValueAssert(value, path, this.jsonMessageConverter); + valueRequirements.accept(() -> valueAssert); + return this.myself; + } + + /** + * Verify that the given JSON {@code path} matches. For paths with an + * operator, this validates that the path expression is valid, but does not + * validate that it yield any results. + * @param path the {@link JsonPath} expression + */ + public SELF hasPath(String path) { + new JsonPathValue(path).assertHasPath(); + return this.myself; + } + + /** + * Verify that the given JSON {@code path} does not match. + * @param path the {@link JsonPath} expression + */ + public SELF doesNotHavePath(String path) { + new JsonPathValue(path).assertDoesNotHavePath(); + return this.myself; + } + + // JsonAssert support + + /** + * 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 .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 + * @param compareMode the compare mode used when checking + */ + public SELF isEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMode) { + String expectedJson = this.jsonLoader.getJson(expected); + return assertNotFailed(compare(expectedJson, compareMode)); + } + + /** + * Verify that the actual value is equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

    + *
  • a {@code byte} array, using {@link ByteArrayResource}
  • + *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • + *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • + *
  • an {@link InputStream}, using {@link InputStreamResource}
  • + *
+ * @param expected a resource containing the expected JSON + * @param compareMode the compare mode used when checking + */ + public SELF isEqualTo(Resource expected, JSONCompareMode compareMode) { + String expectedJson = this.jsonLoader.getJson(expected); + return assertNotFailed(compare(expectedJson, compareMode)); + } + + /** + * 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 .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 + * @param comparator the comparator used when checking + */ + public SELF isEqualTo(@Nullable CharSequence expected, JSONComparator comparator) { + String expectedJson = this.jsonLoader.getJson(expected); + return assertNotFailed(compare(expectedJson, comparator)); + } + + /** + * Verify that the actual value is equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

    + *
  • a {@code byte} array, using {@link ByteArrayResource}
  • + *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • + *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • + *
  • an {@link InputStream}, using {@link InputStreamResource}
  • + *
+ * @param expected a resource containing the expected JSON + * @param comparator the comparator used when checking + */ + public SELF isEqualTo(Resource expected, JSONComparator comparator) { + String expectedJson = this.jsonLoader.getJson(expected); + return assertNotFailed(compare(expectedJson, comparator)); + } + + /** + * Verify that the actual value is {@link JSONCompareMode#LENIENT leniently} + * 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 + */ + public SELF isLenientlyEqualTo(@Nullable CharSequence expected) { + return isEqualTo(expected, JSONCompareMode.LENIENT); + } + + /** + * Verify that the actual value is {@link JSONCompareMode#LENIENT leniently} + * equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

    + *
  • a {@code byte} array, using {@link ByteArrayResource}
  • + *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • + *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • + *
  • an {@link InputStream}, using {@link InputStreamResource}
  • + *
+ * @param expected a resource containing the expected JSON + */ + public SELF isLenientlyEqualTo(Resource expected) { + return isEqualTo(expected, JSONCompareMode.LENIENT); + } + + /** + * Verify that the actual value is {@link 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 + */ + public SELF isStrictlyEqualTo(@Nullable CharSequence expected) { + return isEqualTo(expected, JSONCompareMode.STRICT); + } + + /** + * Verify that the actual value is {@link JSONCompareMode#STRICT strictly} + * equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

    + *
  • a {@code byte} array, using {@link ByteArrayResource}
  • + *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • + *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • + *
  • an {@link InputStream}, using {@link InputStreamResource}
  • + *
+ * @param expected a resource containing the expected JSON + */ + public SELF isStrictlyEqualTo(Resource expected) { + return isEqualTo(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 + * {@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 + * @param compareMode the compare mode used when checking + */ + public SELF isNotEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMode) { + String expectedJson = this.jsonLoader.getJson(expected); + return assertNotPassed(compare(expectedJson, compareMode)); + } + + /** + * Verify that the actual value is not equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

    + *
  • a {@code byte} array, using {@link ByteArrayResource}
  • + *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • + *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • + *
  • an {@link InputStream}, using {@link InputStreamResource}
  • + *
+ * @param expected a resource containing the expected JSON + * @param compareMode the compare mode used when checking + */ + public SELF isNotEqualTo(Resource expected, JSONCompareMode compareMode) { + String expectedJson = this.jsonLoader.getJson(expected); + return assertNotPassed(compare(expectedJson, compareMode)); + } + + /** + * 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 .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 + * @param comparator the comparator used when checking + */ + public SELF isNotEqualTo(@Nullable CharSequence expected, JSONComparator comparator) { + String expectedJson = this.jsonLoader.getJson(expected); + return assertNotPassed(compare(expectedJson, comparator)); + } + + /** + * Verify that the actual value is not equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

    + *
  • a {@code byte} array, using {@link ByteArrayResource}
  • + *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • + *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • + *
  • an {@link InputStream}, using {@link InputStreamResource}
  • + *
+ * @param expected a resource containing the expected JSON + * @param comparator the comparator used when checking + */ + public SELF isNotEqualTo(Resource expected, JSONComparator comparator) { + String expectedJson = this.jsonLoader.getJson(expected); + return assertNotPassed(compare(expectedJson, comparator)); + } + + /** + * Verify that the actual value is not {@link JSONCompareMode#LENIENT + * leniently} 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 + */ + public SELF isNotLenientlyEqualTo(@Nullable CharSequence expected) { + return isNotEqualTo(expected, JSONCompareMode.LENIENT); + } + + /** + * Verify that the actual value is not {@link JSONCompareMode#LENIENT + * leniently} equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

    + *
  • a {@code byte} array, using {@link ByteArrayResource}
  • + *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • + *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • + *
  • an {@link InputStream}, using {@link InputStreamResource}
  • + *
+ * @param expected a resource containing the expected JSON + */ + public SELF isNotLenientlyEqualTo(Resource expected) { + return isNotEqualTo(expected, JSONCompareMode.LENIENT); + } + + /** + * Verify that the actual value is not {@link 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 + */ + public SELF isNotStrictlyEqualTo(@Nullable CharSequence expected) { + return isNotEqualTo(expected, JSONCompareMode.STRICT); + } + + /** + * Verify that the actual value is not {@link JSONCompareMode#STRICT + * strictly} equal to the given JSON {@link Resource}. + *

The resource abstraction allows to provide several input types: + *

    + *
  • a {@code byte} array, using {@link ByteArrayResource}
  • + *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • + *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • + *
  • an {@link InputStream}, using {@link InputStreamResource}
  • + *
+ * @param expected a resource containing the expected JSON + */ + public SELF isNotStrictlyEqualTo(Resource expected) { + return isNotEqualTo(expected, JSONCompareMode.STRICT); + } + + + private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONCompareMode compareMode) { + return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) -> + JSONCompare.compareJSON(expectedJsonString, actualJsonString, compareMode)); + } + + private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONComparator comparator) { + return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) -> + JSONCompare.compareJSON(expectedJsonString, actualJsonString, comparator)); + } + + private JSONCompareResult compare(@Nullable CharSequence actualJson, @Nullable CharSequence expectedJson, + ThrowingBiFunction comparator) { + + if (actualJson == null) { + return compareForNull(expectedJson); + } + if (expectedJson == null) { + return compareForNull(actualJson.toString()); + } + try { + return comparator.applyWithException(actualJson.toString(), expectedJson.toString()); + } + catch (Exception ex) { + if (ex instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new IllegalStateException(ex); + } + } + + private JSONCompareResult compareForNull(@Nullable CharSequence expectedJson) { + JSONCompareResult result = new JSONCompareResult(); + if (expectedJson != null) { + result.fail("Expected null JSON"); + } + return result; + } + + private SELF assertNotFailed(JSONCompareResult result) { + if (result.failed()) { + failWithMessage("JSON comparison failure: %s", result.getMessage()); + } + return this.myself; + } + + private SELF assertNotPassed(JSONCompareResult result) { + if (result.passed()) { + failWithMessage("JSON comparison failure: %s", result.getMessage()); + } + return this.myself; + } + + private AssertionError failure(BasicErrorMessageFactory errorMessageFactory) { + throw failures.failure(this.info, errorMessageFactory); + } + + + /** + * A {@link JsonPath} value. + */ + private class JsonPathValue { + + private final String path; + + private final JsonPath jsonPath; + + private final String json; + + JsonPathValue(String path) { + Assert.hasText(path, "'path' must not be null or empty"); + isNotNull(); + this.path = path; + this.jsonPath = JsonPath.compile(this.path); + this.json = AbstractJsonContentAssert.this.actual; + } + + @Nullable + Object assertHasPath() { + return getValue(); + } + + void assertDoesNotHavePath() { + try { + read(); + throw failure(new JsonPathNotExpected(this.json, this.path)); + } + catch (PathNotFoundException ignore) { + } + } + + @Nullable + Object getValue() { + try { + return read(); + } + catch (PathNotFoundException ex) { + throw failure(new JsonPathNotFound(this.json, this.path)); + } + } + + @Nullable + private Object read() { + return this.jsonPath.read(this.json); + } + + + static final class JsonPathNotFound extends BasicErrorMessageFactory { + + private JsonPathNotFound(String actual, String path) { + super("%nExpecting:%n %s%nTo match JSON path:%n %s%n", actual, path); + } + } + + static final class JsonPathNotExpected extends BasicErrorMessageFactory { + + private JsonPathNotExpected(String actual, String path) { + super("%nExpecting:%n %s%nNot to match JSON path:%n %s%n", actual, path); + } + } + } + +} 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 a81c4724e1f..f36e7ddd7f5 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 @@ -55,7 +55,7 @@ public final class JsonContent implements AssertProvider { */ @Override public JsonContentAssert assertThat() { - return new JsonContentAssert(this.json, this.resourceLoadClass, null); + return new JsonContentAssert(this.json, null, this.resourceLoadClass, null); } /** 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 391c466c3fa..70534f7065f 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,351 +16,36 @@ package org.springframework.test.json; -import java.io.File; -import java.io.InputStream; import java.nio.charset.Charset; -import java.nio.file.Path; -import org.assertj.core.api.AbstractAssert; -import org.skyscreamer.jsonassert.JSONCompare; -import org.skyscreamer.jsonassert.JSONCompareMode; -import org.skyscreamer.jsonassert.JSONCompareResult; -import org.skyscreamer.jsonassert.comparator.JSONComparator; - -import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.FileSystemResource; -import org.springframework.core.io.InputStreamResource; -import org.springframework.core.io.Resource; +import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.lang.Nullable; -import org.springframework.util.function.ThrowingBiFunction; /** - * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied - * to a {@link CharSequence} representation of a JSON document, mostly to - * compare the JSON document against a target, using {@linkplain JSONCompare - * JSON Assert}. + * Default {@link AbstractJsonContentAssert} implementation. * - * @author Phillip Webb - * @author Andy Wilkinson - * @author Diego Berrueta - * @author Camille Vienot * @author Stephane Nicoll * @since 6.2 */ -public class JsonContentAssert extends AbstractAssert { - - private final JsonLoader loader; +public class JsonContentAssert extends AbstractJsonContentAssert { /** - * Create a new {@link JsonContentAssert} instance that will load resources - * relative to the given {@code resourceLoadClass}, using the given - * {@code charset}. - * @param json the actual JSON content + * Create an assert for the given JSON document. + *

Path can be converted to a value object using the given + * {@linkplain GenericHttpMessageConverter json message converter}. + *

Resources to match can be loaded relative to the given + * {@code resourceLoadClass}. If not specified, resources must always be + * absolute. A specific {@link Charset} can be provided if {@code UTF-8} is + * not suitable. + * @param json the JSON document to assert + * @param jsonMessageConverter the converter to use * @param resourceLoadClass the class used to load resources * @param charset the charset of the JSON resources */ - public JsonContentAssert(@Nullable CharSequence json, @Nullable Class resourceLoadClass, - @Nullable Charset charset) { + public JsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter jsonMessageConverter, + @Nullable Class resourceLoadClass, @Nullable Charset charset) { - super(json, JsonContentAssert.class); - this.loader = new JsonLoader(resourceLoadClass, charset); - } - - /** - * Create a new {@link JsonContentAssert} instance that will load resources - * relative to the given {@code resourceLoadClass}, using {@code UTF-8}. - * @param json the actual JSON content - * @param resourceLoadClass the class used to load resources - */ - public JsonContentAssert(@Nullable CharSequence json, @Nullable Class resourceLoadClass) { - this(json, resourceLoadClass, null); - } - - - /** - * 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 .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 - * @param compareMode the compare mode used when checking - */ - public JsonContentAssert isEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMode) { - String expectedJson = this.loader.getJson(expected); - return assertNotFailed(compare(expectedJson, compareMode)); - } - - /** - * Verify that the actual value is equal to the given JSON {@link Resource}. - *

The resource abstraction allows to provide several input types: - *

    - *
  • a {@code byte} array, using {@link ByteArrayResource}
  • - *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • - *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • - *
  • an {@link InputStream}, using {@link InputStreamResource}
  • - *
- * @param expected a resource containing the expected JSON - * @param compareMode the compare mode used when checking - */ - public JsonContentAssert isEqualTo(Resource expected, JSONCompareMode compareMode) { - String expectedJson = this.loader.getJson(expected); - return assertNotFailed(compare(expectedJson, compareMode)); - } - - /** - * 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 .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 - * @param comparator the comparator used when checking - */ - public JsonContentAssert isEqualTo(@Nullable CharSequence expected, JSONComparator comparator) { - String expectedJson = this.loader.getJson(expected); - return assertNotFailed(compare(expectedJson, comparator)); - } - - /** - * Verify that the actual value is equal to the given JSON {@link Resource}. - *

The resource abstraction allows to provide several input types: - *

    - *
  • a {@code byte} array, using {@link ByteArrayResource}
  • - *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • - *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • - *
  • an {@link InputStream}, using {@link InputStreamResource}
  • - *
- * @param expected a resource containing the expected JSON - * @param comparator the comparator used when checking - */ - public JsonContentAssert isEqualTo(Resource expected, JSONComparator comparator) { - String expectedJson = this.loader.getJson(expected); - return assertNotFailed(compare(expectedJson, comparator)); - } - - /** - * Verify that the actual value is {@link JSONCompareMode#LENIENT leniently} - * 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 - */ - public JsonContentAssert isLenientlyEqualTo(@Nullable CharSequence expected) { - return isEqualTo(expected, JSONCompareMode.LENIENT); - } - - /** - * Verify that the actual value is {@link JSONCompareMode#LENIENT leniently} - * equal to the given JSON {@link Resource}. - *

The resource abstraction allows to provide several input types: - *

    - *
  • a {@code byte} array, using {@link ByteArrayResource}
  • - *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • - *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • - *
  • an {@link InputStream}, using {@link InputStreamResource}
  • - *
- * @param expected a resource containing the expected JSON - */ - public JsonContentAssert isLenientlyEqualTo(Resource expected) { - return isEqualTo(expected, JSONCompareMode.LENIENT); - } - - /** - * Verify that the actual value is {@link 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 - */ - public JsonContentAssert isStrictlyEqualTo(@Nullable CharSequence expected) { - return isEqualTo(expected, JSONCompareMode.STRICT); - } - - /** - * Verify that the actual value is {@link JSONCompareMode#STRICT strictly} - * equal to the given JSON {@link Resource}. - *

The resource abstraction allows to provide several input types: - *

    - *
  • a {@code byte} array, using {@link ByteArrayResource}
  • - *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • - *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • - *
  • an {@link InputStream}, using {@link InputStreamResource}
  • - *
- * @param expected a resource containing the expected JSON - */ - public JsonContentAssert isStrictlyEqualTo(Resource expected) { - return isEqualTo(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 - * {@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 - * @param compareMode the compare mode used when checking - */ - public JsonContentAssert isNotEqualTo(@Nullable CharSequence expected, JSONCompareMode compareMode) { - String expectedJson = this.loader.getJson(expected); - return assertNotPassed(compare(expectedJson, compareMode)); - } - - /** - * Verify that the actual value is not equal to the given JSON {@link Resource}. - *

The resource abstraction allows to provide several input types: - *

    - *
  • a {@code byte} array, using {@link ByteArrayResource}
  • - *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • - *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • - *
  • an {@link InputStream}, using {@link InputStreamResource}
  • - *
- * @param expected a resource containing the expected JSON - * @param compareMode the compare mode used when checking - */ - public JsonContentAssert isNotEqualTo(Resource expected, JSONCompareMode compareMode) { - String expectedJson = this.loader.getJson(expected); - return assertNotPassed(compare(expectedJson, compareMode)); - } - - /** - * 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 .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 - * @param comparator the comparator used when checking - */ - public JsonContentAssert isNotEqualTo(@Nullable CharSequence expected, JSONComparator comparator) { - String expectedJson = this.loader.getJson(expected); - return assertNotPassed(compare(expectedJson, comparator)); - } - - /** - * Verify that the actual value is not equal to the given JSON {@link Resource}. - *

The resource abstraction allows to provide several input types: - *

    - *
  • a {@code byte} array, using {@link ByteArrayResource}
  • - *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • - *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • - *
  • an {@link InputStream}, using {@link InputStreamResource}
  • - *
- * @param expected a resource containing the expected JSON - * @param comparator the comparator used when checking - */ - public JsonContentAssert isNotEqualTo(Resource expected, JSONComparator comparator) { - String expectedJson = this.loader.getJson(expected); - return assertNotPassed(compare(expectedJson, comparator)); - } - - /** - * Verify that the actual value is not {@link JSONCompareMode#LENIENT - * leniently} 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 - */ - public JsonContentAssert isNotLenientlyEqualTo(@Nullable CharSequence expected) { - return isNotEqualTo(expected, JSONCompareMode.LENIENT); - } - - /** - * Verify that the actual value is not {@link JSONCompareMode#LENIENT - * leniently} equal to the given JSON {@link Resource}. - *

The resource abstraction allows to provide several input types: - *

    - *
  • a {@code byte} array, using {@link ByteArrayResource}
  • - *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • - *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • - *
  • an {@link InputStream}, using {@link InputStreamResource}
  • - *
- * @param expected a resource containing the expected JSON - */ - public JsonContentAssert isNotLenientlyEqualTo(Resource expected) { - return isNotEqualTo(expected, JSONCompareMode.LENIENT); - } - - /** - * Verify that the actual value is not {@link 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 - */ - public JsonContentAssert isNotStrictlyEqualTo(@Nullable CharSequence expected) { - return isNotEqualTo(expected, JSONCompareMode.STRICT); - } - - /** - * Verify that the actual value is not {@link JSONCompareMode#STRICT - * strictly} equal to the given JSON {@link Resource}. - *

The resource abstraction allows to provide several input types: - *

    - *
  • a {@code byte} array, using {@link ByteArrayResource}
  • - *
  • a {@code classpath} resource, using {@link ClassPathResource}
  • - *
  • a {@link File} or {@link Path}, using {@link FileSystemResource}
  • - *
  • an {@link InputStream}, using {@link InputStreamResource}
  • - *
- * @param expected a resource containing the expected JSON - */ - public JsonContentAssert isNotStrictlyEqualTo(Resource expected) { - return isNotEqualTo(expected, JSONCompareMode.STRICT); - } - - - private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONCompareMode compareMode) { - return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) -> - JSONCompare.compareJSON(expectedJsonString, actualJsonString, compareMode)); - } - - private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONComparator comparator) { - return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) -> - JSONCompare.compareJSON(expectedJsonString, actualJsonString, comparator)); - } - - private JSONCompareResult compare(@Nullable CharSequence actualJson, @Nullable CharSequence expectedJson, - ThrowingBiFunction comparator) { - - if (actualJson == null) { - return compareForNull(expectedJson); - } - if (expectedJson == null) { - return compareForNull(actualJson.toString()); - } - try { - return comparator.applyWithException(actualJson.toString(), expectedJson.toString()); - } - catch (Exception ex) { - if (ex instanceof RuntimeException runtimeException) { - throw runtimeException; - } - throw new IllegalStateException(ex); - } - } - - private JSONCompareResult compareForNull(@Nullable CharSequence expectedJson) { - JSONCompareResult result = new JSONCompareResult(); - if (expectedJson != null) { - result.fail("Expected null JSON"); - } - return result; - } - - private JsonContentAssert assertNotFailed(JSONCompareResult result) { - if (result.failed()) { - failWithMessage("JSON comparison failure: %s", result.getMessage()); - } - return this; - } - - private JsonContentAssert assertNotPassed(JSONCompareResult result) { - if (result.passed()) { - failWithMessage("JSON comparison failure: %s", result.getMessage()); - } - return this; + super(json, jsonMessageConverter, resourceLoadClass, charset, JsonContentAssert.class); } } diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonPathAssert.java b/spring-test/src/main/java/org/springframework/test/json/JsonPathAssert.java deleted file mode 100644 index 2ba28fcafd9..00000000000 --- a/spring-test/src/main/java/org/springframework/test/json/JsonPathAssert.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.test.json; - -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.AssertProvider; -import org.assertj.core.error.BasicErrorMessageFactory; -import org.assertj.core.internal.Failures; - -import org.springframework.http.converter.GenericHttpMessageConverter; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -/** - * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied - * to a {@link CharSequence} representation of a JSON document using - * {@linkplain JsonPath JSON path}. - * - * @author Stephane Nicoll - * @since 6.2 - */ -public class JsonPathAssert extends AbstractAssert { - - private static final Failures failures = Failures.instance(); - - - @Nullable - private final GenericHttpMessageConverter jsonMessageConverter; - - - public JsonPathAssert(CharSequence json, - @Nullable GenericHttpMessageConverter jsonMessageConverter) { - - super(json, JsonPathAssert.class); - this.jsonMessageConverter = jsonMessageConverter; - } - - - /** - * Verify that the given JSON {@code path} is present, and extract the JSON - * value for further {@linkplain JsonPathValueAssert assertions}. - * @param path the {@link JsonPath} expression - * @see #hasPathSatisfying(String, Consumer) - */ - public JsonPathValueAssert extractingPath(String path) { - Object value = new JsonPathValue(path).getValue(); - return new JsonPathValueAssert(value, path, this.jsonMessageConverter); - } - - /** - * Verify that the given JSON {@code path} is present with a JSON value - * satisfying the given {@code valueRequirements}. - * @param path the {@link JsonPath} expression - * @param valueRequirements a {@link Consumer} of the assertion object - */ - public JsonPathAssert hasPathSatisfying(String path, Consumer> valueRequirements) { - Object value = new JsonPathValue(path).assertHasPath(); - JsonPathValueAssert valueAssert = new JsonPathValueAssert(value, path, this.jsonMessageConverter); - valueRequirements.accept(() -> valueAssert); - return this; - } - - /** - * Verify that the given JSON {@code path} matches. For paths with an - * operator, this validates that the path expression is valid, but does not - * validate that it yield any results. - * @param path the {@link JsonPath} expression - */ - public JsonPathAssert hasPath(String path) { - new JsonPathValue(path).assertHasPath(); - return this; - } - - /** - * Verify that the given JSON {@code path} does not match. - * @param path the {@link JsonPath} expression - */ - public JsonPathAssert doesNotHavePath(String path) { - new JsonPathValue(path).assertDoesNotHavePath(); - return this; - } - - - private AssertionError failure(BasicErrorMessageFactory errorMessageFactory) { - throw failures.failure(this.info, errorMessageFactory); - } - - - /** - * A {@link JsonPath} value. - */ - private class JsonPathValue { - - private final String path; - - private final JsonPath jsonPath; - - private final String json; - - JsonPathValue(String path) { - Assert.hasText(path, "'path' must not be null or empty"); - this.path = path; - this.jsonPath = JsonPath.compile(this.path); - this.json = JsonPathAssert.this.actual.toString(); - } - - @Nullable - Object assertHasPath() { - return getValue(); - } - - void assertDoesNotHavePath() { - try { - read(); - throw failure(new JsonPathNotExpected(this.json, this.path)); - } - catch (PathNotFoundException ignore) { - } - } - - @Nullable - Object getValue() { - try { - return read(); - } - catch (PathNotFoundException ex) { - throw failure(new JsonPathNotFound(this.json, this.path)); - } - } - - @Nullable - private Object read() { - return this.jsonPath.read(this.json); - } - - - static final class JsonPathNotFound extends BasicErrorMessageFactory { - - private JsonPathNotFound(String actual, String path) { - super("%nExpecting:%n %s%nTo match JSON path:%n %s%n", actual, path); - } - } - - static final class JsonPathNotExpected extends BasicErrorMessageFactory { - - private JsonPathNotExpected(String actual, String path) { - super("%nExpecting:%n %s%nNot to match JSON path:%n %s%n", actual, path); - } - } - } - -} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java index fffd4831502..202a495c4bb 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java @@ -25,8 +25,8 @@ import org.assertj.core.api.AbstractStringAssert; import org.springframework.core.io.ClassPathResource; import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.lang.Nullable; +import org.springframework.test.json.AbstractJsonContentAssert; import org.springframework.test.json.JsonContentAssert; -import org.springframework.test.json.JsonPathAssert; /** * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to @@ -53,19 +53,20 @@ public class ResponseBodyAssert extends AbstractByteArrayAssert jsonPath() { + return new JsonContentAssert(getJson(), this.jsonMessageConverter, null, this.characterEncoding) + .as("JSON body"); } /** - * Return a new {@linkplain JsonContentAssert assertion} object that provides - * support for {@linkplain org.skyscreamer.jsonassert.JSONCompareMode JSON - * assert} comparisons against expected JSON input which can be loaded from - * the classpath. + * Return a new {@linkplain AbstractJsonContentAssert assertion} object that + * provides support for {@linkplain org.skyscreamer.jsonassert.JSONCompareMode + * JSON assert} comparisons against expected JSON input which can be loaded + * from the classpath. *

This method only supports absolute locations for JSON documents loaded * from the classpath. Consider using {@link #json(Class)} to load JSON * documents relative to a given class. @@ -76,15 +77,15 @@ public class ResponseBodyAssert extends AbstractByteArrayAssert */ - public JsonContentAssert json() { + public AbstractJsonContentAssert json() { return json(null); } /** - * Return a new {@linkplain JsonContentAssert assertion} object that provides - * support for {@linkplain org.skyscreamer.jsonassert.JSONCompareMode JSON - * assert} comparisons against expected JSON input which can be loaded from - * the classpath. + * Return a new {@linkplain AbstractJsonContentAssert assertion} object that + * provides support for {@linkplain org.skyscreamer.jsonassert.JSONCompareMode + * JSON assert} comparisons against expected JSON input which can be loaded + * from the classpath. *

Locations for JSON documents can be absolute using a leading slash, or * relative to the given {@code resourceLoadClass}. *

Example:


@@ -96,8 +97,9 @@ public class ResponseBodyAssert extends AbstractByteArrayAssert resourceLoadClass) {
-		return new JsonContentAssert(getJson(), resourceLoadClass, this.characterEncoding);
+	public AbstractJsonContentAssert json(@Nullable Class resourceLoadClass) {
+		return new JsonContentAssert(getJson(), this.jsonMessageConverter, resourceLoadClass, this.characterEncoding)
+				.as("JSON body");
 	}
 
 	/**
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
new file mode 100644
index 00000000000..5453aebcda9
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/json/AbstractJsonContentAssertTests.java
@@ -0,0 +1,781 @@
+/*
+ * Copyright 2002-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.json;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.assertj.core.api.AssertProvider;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+import org.skyscreamer.jsonassert.comparator.DefaultComparator;
+import org.skyscreamer.jsonassert.comparator.JSONComparator;
+
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.core.io.Resource;
+import org.springframework.http.converter.GenericHttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.lang.Nullable;
+import org.springframework.util.FileCopyUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.assertj.core.api.Assertions.entry;
+
+/**
+ * Tests for {@link AbstractJsonContentAssert}.
+ *
+ * @author Stephane Nicoll
+ * @author Phillip Webb
+ */
+class AbstractJsonContentAssertTests {
+
+	private static final String TYPES = loadJson("types.json");
+
+	private static final String SIMPSONS = loadJson("simpsons.json");
+
+	private static final String NULLS = loadJson("nulls.json");
+
+	private static final String SOURCE = loadJson("source.json");
+
+	private static final String LENIENT_SAME = loadJson("lenient-same.json");
+
+	private static final String DIFFERENT = loadJson("different.json");
+
+	private static final MappingJackson2HttpMessageConverter jsonHttpMessageConverter =
+			new MappingJackson2HttpMessageConverter(new ObjectMapper());
+
+	private static final JSONComparator comparator = new DefaultComparator(JSONCompareMode.LENIENT);
+
+	@Test
+	void isNullWhenActualIsNullShouldPass() {
+		assertThat(forJson(null)).isNull();
+	}
+
+	@Nested
+	class HasPathTests {
+
+		@Test
+		void hasPathForNullJson() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(null)).hasPath("no"))
+					.withMessageContaining("Expecting actual not to be null");
+		}
+
+		@Test
+		void hasPathForPresentAndNotNull() {
+			assertThat(forJson(NULLS)).hasPath("$.valuename");
+		}
+
+		@Test
+		void hasPathForPresentAndNull() {
+			assertThat(forJson(NULLS)).hasPath("$.nullname");
+		}
+
+		@Test
+		void hasPathForOperatorMatching() {
+			assertThat(forJson(SIMPSONS)).
+					hasPath("$.familyMembers[?(@.name == 'Homer')]");
+		}
+
+		@Test
+		void hasPathForOperatorNotMatching() {
+			assertThat(forJson(SIMPSONS)).
+					hasPath("$.familyMembers[?(@.name == 'Dilbert')]");
+		}
+
+		@Test
+		void hasPathForNotPresent() {
+			String expression = "$.missing";
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(NULLS)).hasPath(expression))
+					.satisfies(hasFailedToMatchPath("$.missing"));
+		}
+
+		@Test
+		void hasPathSatisfying() {
+			assertThat(forJson(TYPES)).hasPathSatisfying("$.str", value -> assertThat(value).isEqualTo("foo"))
+					.hasPathSatisfying("$.num", value -> assertThat(value).isEqualTo(5));
+		}
+
+		@Test
+		void hasPathSatisfyingForPathNotPresent() {
+			String expression = "missing";
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(NULLS)).hasPathSatisfying(expression, value -> {}))
+					.satisfies(hasFailedToMatchPath(expression));
+		}
+
+		@Test
+		void doesNotHavePathForMissing() {
+			assertThat(forJson(NULLS)).doesNotHavePath("$.missing");
+		}
+
+
+		@Test
+		void doesNotHavePathForPresent() {
+			String expression = "$.valuename";
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(NULLS)).doesNotHavePath(expression))
+					.satisfies(hasFailedToNotMatchPath(expression));
+		}
+
+	}
+
+	@Nested
+	class ExtractingPathTests {
+
+		@Test
+		void extractingPathForNullJson() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(null)).extractingPath("$"))
+					.withMessageContaining("Expecting actual not to be null");
+		}
+
+		@Test
+		void isNullWithNullPathValue() {
+			assertThat(forJson(NULLS)).extractingPath("$.nullname").isNull();
+		}
+
+		@ParameterizedTest
+		@ValueSource(strings = { "$.str", "$.emptyString", "$.num", "$.bool", "$.arr",
+				"$.emptyArray", "$.colorMap", "$.emptyMap" })
+		void isNotNullWithValue(String path) {
+			assertThat(forJson(TYPES)).extractingPath(path).isNotNull();
+		}
+
+		@ParameterizedTest
+		@MethodSource
+		void isEqualToOnRawValue(String path, Object expected) {
+			assertThat(forJson(TYPES)).extractingPath(path).isEqualTo(expected);
+		}
+
+		static Stream isEqualToOnRawValue() {
+			return Stream.of(
+					Arguments.of("$.str", "foo"),
+					Arguments.of("$.num", 5),
+					Arguments.of("$.bool", true),
+					Arguments.of("$.arr", List.of(42)),
+					Arguments.of("$.colorMap", Map.of("red", "rojo")));
+		}
+
+		@Test
+		void asStringWithActualValue() {
+			assertThat(forJson(TYPES)).extractingPath("@.str").asString().startsWith("f").endsWith("o");
+		}
+
+		@Test
+		void asStringIsEmpty() {
+			assertThat(forJson(TYPES)).extractingPath("@.emptyString").asString().isEmpty();
+		}
+
+		@Test
+		void asNumberWithActualValue() {
+			assertThat(forJson(TYPES)).extractingPath("@.num").asNumber().isEqualTo(5);
+		}
+
+		@Test
+		void asBooleanWithActualValue() {
+			assertThat(forJson(TYPES)).extractingPath("@.bool").asBoolean().isTrue();
+		}
+
+		@Test
+		void asArrayWithActualValue() {
+			assertThat(forJson(TYPES)).extractingPath("@.arr").asArray().containsOnly(42);
+		}
+
+		@Test
+		void asArrayIsEmpty() {
+			assertThat(forJson(TYPES)).extractingPath("@.emptyArray").asArray().isEmpty();
+		}
+
+		@Test
+		void asArrayWithFilterPredicatesMatching() {
+			assertThat(forJson(SIMPSONS))
+					.extractingPath("$.familyMembers[?(@.name == 'Bart')]").asArray().hasSize(1);
+		}
+
+		@Test
+		void asArrayWithFilterPredicatesNotMatching() {
+			assertThat(forJson(SIMPSONS)).
+					extractingPath("$.familyMembers[?(@.name == 'Dilbert')]").asArray().isEmpty();
+		}
+
+		@Test
+		void asMapWithActualValue() {
+			assertThat(forJson(TYPES)).extractingPath("@.colorMap").asMap().containsOnly(entry("red", "rojo"));
+		}
+
+		@Test
+		void asMapIsEmpty() {
+			assertThat(forJson(TYPES)).extractingPath("@.emptyMap").asMap().isEmpty();
+		}
+
+		@Test
+		void convertToWithoutHttpMessageConverterShouldFail() {
+			JsonPathValueAssert path = assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[0]");
+			assertThatIllegalStateException()
+					.isThrownBy(() -> path.convertTo(ExtractingPathTests.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)
+					.satisfies(member -> assertThat(member.name).isEqualTo("Homer"));
+		}
+
+		@Test
+		void convertToIncompatibleTargetTypeShouldFail() {
+			JsonPathValueAssert path = assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
+					.extractingPath("$.familyMembers[0]");
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> path.convertTo(ExtractingPathTests.Customer.class))
+					.withMessageContainingAll("Expected value at JSON path \"$.familyMembers[0]\":",
+							Customer.class.getName(), "name");
+		}
+
+		@Test
+		void convertArrayToParameterizedType() {
+			assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
+					.extractingPath("$.familyMembers")
+					.convertTo(new ParameterizedTypeReference>() {})
+					.satisfies(family -> assertThat(family).hasSize(5).element(0).isEqualTo(new Member("Homer")));
+		}
+
+		@Test
+		void isEmptyWithPathHavingNullValue() {
+			assertThat(forJson(NULLS)).extractingPath("nullname").isEmpty();
+		}
+
+		@ParameterizedTest
+		@ValueSource(strings = { "$.emptyString", "$.emptyArray", "$.emptyMap" })
+		void isEmptyWithEmptyValue(String path) {
+			assertThat(forJson(TYPES)).extractingPath(path).isEmpty();
+		}
+
+		@Test
+		void isEmptyForPathWithFilterMatching() {
+			String expression = "$.familyMembers[?(@.name == 'Bart')]";
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SIMPSONS)).extractingPath(expression).isEmpty())
+					.withMessageContainingAll("Expected value at JSON path \"" + expression + "\"",
+							"[{\"name\":\"Bart\"}]", "To be empty");
+		}
+
+		@Test
+		void isEmptyForPathWithFilterNotMatching() {
+			assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[?(@.name == 'Dilbert')]").isEmpty();
+		}
+
+		@ParameterizedTest
+		@ValueSource(strings = { "$.str", "$.num", "$.bool", "$.arr", "$.colorMap" })
+		void isNotEmptyWithNonNullValue(String path) {
+			assertThat(forJson(TYPES)).extractingPath(path).isNotEmpty();
+		}
+
+		@Test
+		void isNotEmptyForPathWithFilterMatching() {
+			assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[?(@.name == 'Bart')]").isNotEmpty();
+		}
+
+		@Test
+		void isNotEmptyForPathWithFilterNotMatching() {
+			String expression = "$.familyMembers[?(@.name == 'Dilbert')]";
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SIMPSONS)).extractingPath(expression).isNotEmpty())
+					.withMessageContainingAll("Expected value at JSON path \"" + expression + "\"",
+							"To not be empty");
+		}
+
+
+		private record Member(String name) {}
+
+		private record Customer(long id, String username) {}
+
+		private AssertProvider> forJson(@Nullable String json) {
+			return () -> new TestJsonContentAssert(json, null, null, null);
+		}
+
+		private AssertProvider> forJson(@Nullable String json, GenericHttpMessageConverter jsonHttpMessageConverter) {
+			return () -> new TestJsonContentAssert(json, jsonHttpMessageConverter, null, null);
+		}
+
+	}
+
+	@Nested
+	class EqualsNotEqualsTests {
+
+		@Test
+		void isEqualToWhenStringIsMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isEqualTo(SOURCE);
+		}
+
+		@Test
+		void isEqualToWhenNullActualShouldFail() {
+			assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
+					assertThat(forJson(null)).isEqualTo(SOURCE));
+		}
+
+		@Test
+		void isEqualToWhenExpectedIsNotAStringShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(SOURCE.getBytes()));
+		}
+	}
+
+	@Nested
+	@TestInstance(Lifecycle.PER_CLASS)
+	class JsonAssertTests {
+
+		@Test
+		void isEqualToWhenExpectedIsNullShouldFail() {
+			CharSequence actual = null;
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(actual, JSONCompareMode.LENIENT));
+		}
+
+		@Test
+		void isEqualToWhenStringIsMatchingAndLenientShouldPass() {
+			assertThat(forJson(SOURCE)).isEqualTo(LENIENT_SAME, JSONCompareMode.LENIENT);
+		}
+
+		@Test
+		void isEqualToWhenStringIsNotMatchingAndLenientShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(DIFFERENT, JSONCompareMode.LENIENT));
+		}
+
+		@Test
+		void isEqualToWhenResourcePathIsMatchingAndLenientShouldPass() {
+			assertThat(forJson(SOURCE)).isEqualTo("lenient-same.json", JSONCompareMode.LENIENT);
+		}
+
+		@Test
+		void isEqualToWhenResourcePathIsNotMatchingAndLenientShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo("different.json", JSONCompareMode.LENIENT));
+		}
+
+		Stream source() {
+			return Stream.of(
+					Arguments.of(new ClassPathResource("source.json", AbstractJsonContentAssertTests.class)),
+					Arguments.of(new ByteArrayResource(SOURCE.getBytes())),
+					Arguments.of(new FileSystemResource(createFile(SOURCE))),
+					Arguments.of(new InputStreamResource(createInputStream(SOURCE))));
+		}
+
+		Stream lenientSame() {
+			return Stream.of(
+					Arguments.of(new ClassPathResource("lenient-same.json", AbstractJsonContentAssertTests.class)),
+					Arguments.of(new ByteArrayResource(LENIENT_SAME.getBytes())),
+					Arguments.of(new FileSystemResource(createFile(LENIENT_SAME))),
+					Arguments.of(new InputStreamResource(createInputStream(LENIENT_SAME))));
+		}
+
+		Stream different() {
+			return Stream.of(
+					Arguments.of(new ClassPathResource("different.json", AbstractJsonContentAssertTests.class)),
+					Arguments.of(new ByteArrayResource(DIFFERENT.getBytes())),
+					Arguments.of(new FileSystemResource(createFile(DIFFERENT))),
+					Arguments.of(new InputStreamResource(createInputStream(DIFFERENT))));
+		}
+
+		@ParameterizedTest
+		@MethodSource("lenientSame")
+		void isEqualToWhenResourceIsMatchingAndLenientSameShouldPass(Resource expected) {
+			assertThat(forJson(SOURCE)).isEqualTo(expected, JSONCompareMode.LENIENT);
+		}
+
+		@ParameterizedTest
+		@MethodSource("different")
+		void isEqualToWhenResourceIsNotMatchingAndLenientShouldFail(Resource expected) {
+			assertThatExceptionOfType(AssertionError.class).isThrownBy(
+					() -> assertThat(forJson(SOURCE)).isEqualTo(expected, JSONCompareMode.LENIENT));
+		}
+
+		@Test
+		void isEqualToWhenStringIsMatchingAndComparatorShouldPass() {
+			assertThat(forJson(SOURCE)).isEqualTo(LENIENT_SAME, comparator);
+		}
+
+		@Test
+		void isEqualToWhenStringIsNotMatchingAndComparatorShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(DIFFERENT, comparator));
+		}
+
+		@Test
+		void isEqualToWhenResourcePathIsMatchingAndComparatorShouldPass() {
+			assertThat(forJson(SOURCE)).isEqualTo("lenient-same.json", comparator);
+		}
+
+		@Test
+		void isEqualToWhenResourcePathIsNotMatchingAndComparatorShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo("different.json", comparator));
+		}
+
+		@ParameterizedTest
+		@MethodSource("lenientSame")
+		void isEqualToWhenResourceIsMatchingAndComparatorShouldPass(Resource expected) {
+			assertThat(forJson(SOURCE)).isEqualTo(expected, comparator);
+		}
+
+		@ParameterizedTest
+		@MethodSource("different")
+		void isEqualToWhenResourceIsNotMatchingAndComparatorShouldFail(Resource expected) {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(expected, comparator));
+		}
+
+		@Test
+		void isLenientlyEqualToWhenStringIsMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isLenientlyEqualTo(LENIENT_SAME);
+		}
+
+		@Test
+		void isLenientlyEqualToWhenNullActualShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(null)).isLenientlyEqualTo(SOURCE));
+		}
+
+		@Test
+		void isLenientlyEqualToWhenStringIsNotMatchingShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo(DIFFERENT));
+		}
+
+		@Test
+		void isLenientlyEqualToWhenExpectedDoesNotExistShouldFail() {
+			assertThatIllegalStateException()
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo("does-not-exist.json"))
+					.withMessage("Unable to load JSON from class path resource [org/springframework/test/json/does-not-exist.json]");
+		}
+
+		@Test
+		void isLenientlyEqualToWhenResourcePathIsMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isLenientlyEqualTo("lenient-same.json");
+		}
+
+		@Test
+		void isLenientlyEqualToWhenResourcePathIsNotMatchingShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo("different.json"));
+		}
+
+		@ParameterizedTest
+		@MethodSource("lenientSame")
+		void isLenientlyEqualToWhenResourceIsMatchingShouldPass(Resource expected) {
+			assertThat(forJson(SOURCE)).isLenientlyEqualTo(expected);
+		}
+
+		@ParameterizedTest
+		@MethodSource("different")
+		void isLenientlyEqualToWhenResourceIsNotMatchingShouldFail(Resource expected) {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo(expected));
+		}
+
+		@Test
+		void isStrictlyEqualToWhenStringIsMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isStrictlyEqualTo(SOURCE);
+		}
+
+		@Test
+		void isStrictlyEqualToWhenStringIsNotMatchingShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo(LENIENT_SAME));
+		}
+
+		@Test
+		void isStrictlyEqualToWhenResourcePathIsMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isStrictlyEqualTo("source.json");
+		}
+
+		@Test
+		void isStrictlyEqualToWhenResourcePathIsNotMatchingShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo("lenient-same.json"));
+		}
+
+		@ParameterizedTest
+		@MethodSource("source")
+		void isStrictlyEqualToWhenResourceIsMatchingShouldPass(Resource expected) {
+			assertThat(forJson(SOURCE)).isStrictlyEqualTo(expected);
+		}
+
+		@ParameterizedTest
+		@MethodSource("lenientSame")
+		void isStrictlyEqualToWhenResourceIsNotMatchingShouldFail(Resource expected) {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo(expected));
+		}
+
+
+		@Test
+		void isNotEqualToWhenStringIsMatchingShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(SOURCE));
+		}
+
+		@Test
+		void isNotEqualToWhenNullActualShouldPass() {
+			assertThat(forJson(null)).isNotEqualTo(SOURCE);
+		}
+
+		@Test
+		void isNotEqualToWhenStringIsNotMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT);
+		}
+
+		@Test
+		void isNotEqualToAsObjectWhenExpectedIsNotAStringShouldNotFail() {
+			assertThat(forJson(SOURCE)).isNotEqualTo(SOURCE.getBytes());
+		}
+
+		@Test
+		void isNotEqualToWhenStringIsMatchingAndLenientShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(LENIENT_SAME, JSONCompareMode.LENIENT));
+		}
+
+		@Test
+		void isNotEqualToWhenStringIsNotMatchingAndLenientShouldPass() {
+			assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT, JSONCompareMode.LENIENT);
+		}
+
+		@Test
+		void isNotEqualToWhenResourcePathIsMatchingAndLenientShouldFail() {
+			assertThatExceptionOfType(AssertionError.class).isThrownBy(
+					() -> assertThat(forJson(SOURCE)).isNotEqualTo("lenient-same.json", JSONCompareMode.LENIENT));
+		}
+
+		@Test
+		void isNotEqualToWhenResourcePathIsNotMatchingAndLenientShouldPass() {
+			assertThat(forJson(SOURCE)).isNotEqualTo("different.json", JSONCompareMode.LENIENT);
+		}
+
+		@ParameterizedTest
+		@MethodSource("lenientSame")
+		void isNotEqualToWhenResourceIsMatchingAndLenientShouldFail(Resource expected) {
+			assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(forJson(SOURCE))
+					.isNotEqualTo(expected, JSONCompareMode.LENIENT));
+		}
+
+		@ParameterizedTest
+		@MethodSource("different")
+		void isNotEqualToWhenResourceIsNotMatchingAndLenientShouldPass(Resource expected) {
+			assertThat(forJson(SOURCE)).isNotEqualTo(expected, JSONCompareMode.LENIENT);
+		}
+
+		@Test
+		void isNotEqualToWhenStringIsMatchingAndComparatorShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(LENIENT_SAME, comparator));
+		}
+
+		@Test
+		void isNotEqualToWhenStringIsNotMatchingAndComparatorShouldPass() {
+			assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT, comparator);
+		}
+
+		@Test
+		void isNotEqualToWhenResourcePathIsMatchingAndComparatorShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo("lenient-same.json", comparator));
+		}
+
+		@Test
+		void isNotEqualToWhenResourcePathIsNotMatchingAndComparatorShouldPass() {
+			assertThat(forJson(SOURCE)).isNotEqualTo("different.json", comparator);
+		}
+
+		@ParameterizedTest
+		@MethodSource("lenientSame")
+		void isNotEqualToWhenResourceIsMatchingAndComparatorShouldFail(Resource expected) {
+			assertThatExceptionOfType(AssertionError.class).isThrownBy(
+					() -> assertThat(forJson(SOURCE)).isNotEqualTo(expected, comparator));
+		}
+
+		@ParameterizedTest
+		@MethodSource("different")
+		void isNotEqualToWhenResourceIsNotMatchingAndComparatorShouldPass(Resource expected) {
+			assertThat(forJson(SOURCE)).isNotEqualTo(expected, comparator);
+		}
+
+		@Test
+		void isNotEqualToWhenResourceIsMatchingAndComparatorShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(createResource(LENIENT_SAME), comparator));
+		}
+
+		@Test
+		void isNotEqualToWhenResourceIsNotMatchingAndComparatorShouldPass() {
+			assertThat(forJson(SOURCE)).isNotEqualTo(createResource(DIFFERENT), comparator);
+		}
+
+		@Test
+		void isNotLenientlyEqualToWhenNullActualShouldPass() {
+			assertThat(forJson(null)).isNotLenientlyEqualTo(SOURCE);
+		}
+
+		@Test
+		void isNotLenientlyEqualToWhenStringIsNotMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(DIFFERENT);
+		}
+
+		@Test
+		void isNotLenientlyEqualToWhenResourcePathIsMatchingShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotLenientlyEqualTo("lenient-same.json"));
+		}
+
+		@Test
+		void isNotLenientlyEqualToWhenResourcePathIsNotMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isNotLenientlyEqualTo("different.json");
+		}
+
+		@ParameterizedTest
+		@MethodSource("lenientSame")
+		void isNotLenientlyEqualToWhenResourceIsMatchingShouldFail(Resource expected) {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(expected));
+		}
+
+		@ParameterizedTest
+		@MethodSource("different")
+		void isNotLenientlyEqualToWhenResourceIsNotMatchingShouldPass(Resource expected) {
+			assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(expected);
+		}
+
+		@Test
+		void isNotStrictlyEqualToWhenStringIsMatchingShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(SOURCE));
+		}
+
+		@Test
+		void isNotStrictlyEqualToWhenStringIsNotMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(LENIENT_SAME);
+		}
+
+		@Test
+		void isNotStrictlyEqualToWhenResourcePathIsMatchingShouldFail() {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo("source.json"));
+		}
+
+		@Test
+		void isNotStrictlyEqualToWhenResourcePathIsNotMatchingShouldPass() {
+			assertThat(forJson(SOURCE)).isNotStrictlyEqualTo("lenient-same.json");
+		}
+
+		@ParameterizedTest
+		@MethodSource("source")
+		void isNotStrictlyEqualToWhenResourceIsMatchingShouldFail(Resource expected) {
+			assertThatExceptionOfType(AssertionError.class)
+					.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(expected));
+		}
+
+		@ParameterizedTest
+		@MethodSource("lenientSame")
+		void isNotStrictlyEqualToWhenResourceIsNotMatchingShouldPass(Resource expected) {
+			assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(expected);
+		}
+
+		private AssertProvider> forJson(@Nullable String json) {
+			return () -> new TestJsonContentAssert(json, null, getClass(), null);
+		}
+
+	}
+
+
+	private Consumer hasFailedToMatchPath(String expression) {
+		return error -> assertThat(error.getMessage()).containsSubsequence("Expecting:",
+				"To match JSON path:", "\"" + expression + "\"");
+	}
+
+	private Consumer hasFailedToNotMatchPath(String expression) {
+		return error -> assertThat(error.getMessage()).containsSubsequence("Expecting:",
+				"Not to match JSON path:", "\"" + expression + "\"");
+	}
+
+	private Path createFile(String content) {
+		try {
+			Path temp = Files.createTempFile("file", ".json");
+			Files.writeString(temp, content);
+			return temp;
+		}
+		catch (IOException ex) {
+			throw new IllegalStateException(ex);
+		}
+	}
+
+	private InputStream createInputStream(String content) {
+		return new ByteArrayInputStream(content.getBytes());
+	}
+
+	private Resource createResource(String content) {
+		return new ByteArrayResource(content.getBytes());
+	}
+
+	private static String loadJson(String path) {
+		try {
+			ClassPathResource resource = new ClassPathResource(path, AbstractJsonContentAssertTests.class);
+			return new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
+		}
+		catch (Exception ex) {
+			throw new IllegalStateException(ex);
+		}
+
+	}
+
+	private AssertProvider> forJson(@Nullable String json) {
+		return () -> new TestJsonContentAssert(json, null, null, null);
+	}
+
+	private static class TestJsonContentAssert extends AbstractJsonContentAssert {
+
+		public TestJsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter jsonMessageConverter, @Nullable Class resourceLoadClass, @Nullable Charset charset) {
+			super(json, jsonMessageConverter, resourceLoadClass, charset, TestJsonContentAssert.class);
+		}
+	}
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/json/JsonContentAssertTests.java b/spring-test/src/test/java/org/springframework/test/json/JsonContentAssertTests.java
deleted file mode 100644
index 02c839bd8e0..00000000000
--- a/spring-test/src/test/java/org/springframework/test/json/JsonContentAssertTests.java
+++ /dev/null
@@ -1,479 +0,0 @@
-/*
- * Copyright 2002-2024 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.test.json;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.stream.Stream;
-
-import org.assertj.core.api.AssertProvider;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.TestInstance;
-import org.junit.jupiter.api.TestInstance.Lifecycle;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-import org.skyscreamer.jsonassert.JSONCompareMode;
-import org.skyscreamer.jsonassert.comparator.DefaultComparator;
-import org.skyscreamer.jsonassert.comparator.JSONComparator;
-
-import org.springframework.core.io.ByteArrayResource;
-import org.springframework.core.io.ClassPathResource;
-import org.springframework.core.io.FileSystemResource;
-import org.springframework.core.io.InputStreamResource;
-import org.springframework.core.io.Resource;
-import org.springframework.lang.Nullable;
-import org.springframework.util.FileCopyUtils;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
-
-/**
- * Tests for {@link JsonContentAssert}.
- *
- * @author Stephane Nicoll
- * @author Phillip Webb
- */
-@TestInstance(Lifecycle.PER_CLASS)
-class JsonContentAssertTests {
-
-	private static final String SOURCE = loadJson("source.json");
-
-	private static final String LENIENT_SAME = loadJson("lenient-same.json");
-
-	private static final String DIFFERENT = loadJson("different.json");
-
-	private static final JSONComparator COMPARATOR = new DefaultComparator(JSONCompareMode.LENIENT);
-
-	@Test
-	void isEqualToWhenStringIsMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isEqualTo(SOURCE);
-	}
-
-	@Test
-	void isEqualToWhenNullActualShouldFail() {
-		assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
-				assertThat(forJson(null)).isEqualTo(SOURCE));
-	}
-
-	@Test
-	void isEqualToWhenExpectedIsNotAStringShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(SOURCE.getBytes()));
-	}
-
-	@Test
-	void isEqualToWhenExpectedIsNullShouldFail() {
-		CharSequence actual = null;
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(actual, JSONCompareMode.LENIENT));
-	}
-
-	@Test
-	void isEqualToWhenStringIsMatchingAndLenientShouldPass() {
-		assertThat(forJson(SOURCE)).isEqualTo(LENIENT_SAME, JSONCompareMode.LENIENT);
-	}
-
-	@Test
-	void isEqualToWhenStringIsNotMatchingAndLenientShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(DIFFERENT, JSONCompareMode.LENIENT));
-	}
-
-	@Test
-	void isEqualToWhenResourcePathIsMatchingAndLenientShouldPass() {
-		assertThat(forJson(SOURCE)).isEqualTo("lenient-same.json", JSONCompareMode.LENIENT);
-	}
-
-	@Test
-	void isEqualToWhenResourcePathIsNotMatchingAndLenientShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo("different.json", JSONCompareMode.LENIENT));
-	}
-
-	Stream source() {
-		return Stream.of(
-				Arguments.of(new ClassPathResource("source.json", JsonContentAssertTests.class)),
-				Arguments.of(new ByteArrayResource(SOURCE.getBytes())),
-				Arguments.of(new FileSystemResource(createFile(SOURCE))),
-				Arguments.of(new InputStreamResource(createInputStream(SOURCE))));
-	}
-
-	Stream lenientSame() {
-		return Stream.of(
-				Arguments.of(new ClassPathResource("lenient-same.json", JsonContentAssertTests.class)),
-				Arguments.of(new ByteArrayResource(LENIENT_SAME.getBytes())),
-				Arguments.of(new FileSystemResource(createFile(LENIENT_SAME))),
-				Arguments.of(new InputStreamResource(createInputStream(LENIENT_SAME))));
-	}
-
-	Stream different() {
-		return Stream.of(
-				Arguments.of(new ClassPathResource("different.json", JsonContentAssertTests.class)),
-				Arguments.of(new ByteArrayResource(DIFFERENT.getBytes())),
-				Arguments.of(new FileSystemResource(createFile(DIFFERENT))),
-				Arguments.of(new InputStreamResource(createInputStream(DIFFERENT))));
-	}
-
-	@ParameterizedTest
-	@MethodSource("lenientSame")
-	void isEqualToWhenResourceIsMatchingAndLenientSameShouldPass(Resource expected) {
-		assertThat(forJson(SOURCE)).isEqualTo(expected, JSONCompareMode.LENIENT);
-	}
-
-	@ParameterizedTest
-	@MethodSource("different")
-	void isEqualToWhenResourceIsNotMatchingAndLenientShouldFail(Resource expected) {
-		assertThatExceptionOfType(AssertionError.class).isThrownBy(
-				() -> assertThat(forJson(SOURCE)).isEqualTo(expected, JSONCompareMode.LENIENT));
-	}
-
-
-	@Test
-	void isEqualToWhenStringIsMatchingAndComparatorShouldPass() {
-		assertThat(forJson(SOURCE)).isEqualTo(LENIENT_SAME, COMPARATOR);
-	}
-
-	@Test
-	void isEqualToWhenStringIsNotMatchingAndComparatorShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(DIFFERENT, COMPARATOR));
-	}
-
-	@Test
-	void isEqualToWhenResourcePathIsMatchingAndComparatorShouldPass() {
-		assertThat(forJson(SOURCE)).isEqualTo("lenient-same.json", COMPARATOR);
-	}
-
-	@Test
-	void isEqualToWhenResourcePathIsNotMatchingAndComparatorShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo("different.json", COMPARATOR));
-	}
-
-	@ParameterizedTest
-	@MethodSource("lenientSame")
-	void isEqualToWhenResourceIsMatchingAndComparatorShouldPass(Resource expected) {
-		assertThat(forJson(SOURCE)).isEqualTo(expected, COMPARATOR);
-	}
-
-	@ParameterizedTest
-	@MethodSource("different")
-	void isEqualToWhenResourceIsNotMatchingAndComparatorShouldFail(Resource expected) {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isEqualTo(expected, COMPARATOR));
-	}
-
-	@Test
-	void isLenientlyEqualToWhenStringIsMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isLenientlyEqualTo(LENIENT_SAME);
-	}
-
-	@Test
-	void isLenientlyEqualToWhenNullActualShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(null)).isLenientlyEqualTo(SOURCE));
-	}
-
-	@Test
-	void isLenientlyEqualToWhenStringIsNotMatchingShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo(DIFFERENT));
-	}
-
-	@Test
-	void isLenientlyEqualToWhenExpectedDoesNotExistShouldFail() {
-		assertThatIllegalStateException()
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo("does-not-exist.json"))
-				.withMessage("Unable to load JSON from class path resource [org/springframework/test/json/does-not-exist.json]");
-	}
-
-	@Test
-	void isLenientlyEqualToWhenResourcePathIsMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isLenientlyEqualTo("lenient-same.json");
-	}
-
-	@Test
-	void isLenientlyEqualToWhenResourcePathIsNotMatchingShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo("different.json"));
-	}
-
-	@ParameterizedTest
-	@MethodSource("lenientSame")
-	void isLenientlyEqualToWhenResourceIsMatchingShouldPass(Resource expected) {
-		assertThat(forJson(SOURCE)).isLenientlyEqualTo(expected);
-	}
-
-	@ParameterizedTest
-	@MethodSource("different")
-	void isLenientlyEqualToWhenResourceIsNotMatchingShouldFail(Resource expected) {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isLenientlyEqualTo(expected));
-	}
-
-	@Test
-	void isStrictlyEqualToWhenStringIsMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isStrictlyEqualTo(SOURCE);
-	}
-
-	@Test
-	void isStrictlyEqualToWhenStringIsNotMatchingShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo(LENIENT_SAME));
-	}
-
-	@Test
-	void isStrictlyEqualToWhenResourcePathIsMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isStrictlyEqualTo("source.json");
-	}
-
-	@Test
-	void isStrictlyEqualToWhenResourcePathIsNotMatchingShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo("lenient-same.json"));
-	}
-
-	@ParameterizedTest
-	@MethodSource("source")
-	void isStrictlyEqualToWhenResourceIsMatchingShouldPass(Resource expected) {
-		assertThat(forJson(SOURCE)).isStrictlyEqualTo(expected);
-	}
-
-	@ParameterizedTest
-	@MethodSource("lenientSame")
-	void isStrictlyEqualToWhenResourceIsNotMatchingShouldFail(Resource expected) {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo(expected));
-	}
-
-
-	@Test
-	void isNotEqualToWhenStringIsMatchingShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(SOURCE));
-	}
-
-	@Test
-	void isNotEqualToWhenNullActualShouldPass() {
-		assertThat(forJson(null)).isNotEqualTo(SOURCE);
-	}
-
-	@Test
-	void isNotEqualToWhenStringIsNotMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT);
-	}
-
-	@Test
-	void isNotEqualToAsObjectWhenExpectedIsNotAStringShouldNotFail() {
-		assertThat(forJson(SOURCE)).isNotEqualTo(SOURCE.getBytes());
-	}
-
-	@Test
-	void isNotEqualToWhenStringIsMatchingAndLenientShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(LENIENT_SAME, JSONCompareMode.LENIENT));
-	}
-
-	@Test
-	void isNotEqualToWhenStringIsNotMatchingAndLenientShouldPass() {
-		assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT, JSONCompareMode.LENIENT);
-	}
-
-	@Test
-	void isNotEqualToWhenResourcePathIsMatchingAndLenientShouldFail() {
-		assertThatExceptionOfType(AssertionError.class).isThrownBy(
-				() -> assertThat(forJson(SOURCE)).isNotEqualTo("lenient-same.json", JSONCompareMode.LENIENT));
-	}
-
-	@Test
-	void isNotEqualToWhenResourcePathIsNotMatchingAndLenientShouldPass() {
-		assertThat(forJson(SOURCE)).isNotEqualTo("different.json", JSONCompareMode.LENIENT);
-	}
-
-	@ParameterizedTest
-	@MethodSource("lenientSame")
-	void isNotEqualToWhenResourceIsMatchingAndLenientShouldFail(Resource expected) {
-		assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(forJson(SOURCE))
-				.isNotEqualTo(expected, JSONCompareMode.LENIENT));
-	}
-
-	@ParameterizedTest
-	@MethodSource("different")
-	void isNotEqualToWhenResourceIsNotMatchingAndLenientShouldPass(Resource expected) {
-		assertThat(forJson(SOURCE)).isNotEqualTo(expected, JSONCompareMode.LENIENT);
-	}
-
-	@Test
-	void isNotEqualToWhenStringIsMatchingAndComparatorShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(LENIENT_SAME, COMPARATOR));
-	}
-
-	@Test
-	void isNotEqualToWhenStringIsNotMatchingAndComparatorShouldPass() {
-		assertThat(forJson(SOURCE)).isNotEqualTo(DIFFERENT, COMPARATOR);
-	}
-
-	@Test
-	void isNotEqualToWhenResourcePathIsMatchingAndComparatorShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo("lenient-same.json", COMPARATOR));
-	}
-
-	@Test
-	void isNotEqualToWhenResourcePathIsNotMatchingAndComparatorShouldPass() {
-		assertThat(forJson(SOURCE)).isNotEqualTo("different.json", COMPARATOR);
-	}
-
-	@ParameterizedTest
-	@MethodSource("lenientSame")
-	void isNotEqualToWhenResourceIsMatchingAndComparatorShouldFail(Resource expected) {
-		assertThatExceptionOfType(AssertionError.class).isThrownBy(
-				() -> assertThat(forJson(SOURCE)).isNotEqualTo(expected, COMPARATOR));
-	}
-
-	@ParameterizedTest
-	@MethodSource("different")
-	void isNotEqualToWhenResourceIsNotMatchingAndComparatorShouldPass(Resource expected) {
-		assertThat(forJson(SOURCE)).isNotEqualTo(expected, COMPARATOR);
-	}
-
-	@Test
-	void isNotEqualToWhenResourceIsMatchingAndComparatorShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotEqualTo(createResource(LENIENT_SAME), COMPARATOR));
-	}
-
-	@Test
-	void isNotEqualToWhenResourceIsNotMatchingAndComparatorShouldPass() {
-		assertThat(forJson(SOURCE)).isNotEqualTo(createResource(DIFFERENT), COMPARATOR);
-	}
-
-	@Test
-	void isNotLenientlyEqualToWhenNullActualShouldPass() {
-		assertThat(forJson(null)).isNotLenientlyEqualTo(SOURCE);
-	}
-
-	@Test
-	void isNotLenientlyEqualToWhenStringIsNotMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(DIFFERENT);
-	}
-
-	@Test
-	void isNotLenientlyEqualToWhenResourcePathIsMatchingShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotLenientlyEqualTo("lenient-same.json"));
-	}
-
-	@Test
-	void isNotLenientlyEqualToWhenResourcePathIsNotMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isNotLenientlyEqualTo("different.json");
-	}
-
-	@ParameterizedTest
-	@MethodSource("lenientSame")
-	void isNotLenientlyEqualToWhenResourceIsMatchingShouldFail(Resource expected) {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(expected));
-	}
-
-	@ParameterizedTest
-	@MethodSource("different")
-	void isNotLenientlyEqualToWhenResourceIsNotMatchingShouldPass(Resource expected) {
-		assertThat(forJson(SOURCE)).isNotLenientlyEqualTo(expected);
-	}
-
-	@Test
-	void isNotStrictlyEqualToWhenStringIsMatchingShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(SOURCE));
-	}
-
-	@Test
-	void isNotStrictlyEqualToWhenStringIsNotMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(LENIENT_SAME);
-	}
-
-	@Test
-	void isNotStrictlyEqualToWhenResourcePathIsMatchingShouldFail() {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo("source.json"));
-	}
-
-	@Test
-	void isNotStrictlyEqualToWhenResourcePathIsNotMatchingShouldPass() {
-		assertThat(forJson(SOURCE)).isNotStrictlyEqualTo("lenient-same.json");
-	}
-
-	@ParameterizedTest
-	@MethodSource("source")
-	void isNotStrictlyEqualToWhenResourceIsMatchingShouldFail(Resource expected) {
-		assertThatExceptionOfType(AssertionError.class)
-				.isThrownBy(() -> assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(expected));
-	}
-
-	@ParameterizedTest
-	@MethodSource("lenientSame")
-	void isNotStrictlyEqualToWhenResourceIsNotMatchingShouldPass(Resource expected) {
-		assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(expected);
-	}
-
-	@Test
-	void isNullWhenActualIsNullShouldPass() {
-		assertThat(forJson(null)).isNull();
-	}
-
-	private Path createFile(String content) {
-		try {
-			Path temp = Files.createTempFile("file", ".json");
-			Files.writeString(temp, content);
-			return temp;
-		}
-		catch (IOException ex) {
-			throw new IllegalStateException(ex);
-		}
-	}
-
-	private InputStream createInputStream(String content) {
-		return new ByteArrayInputStream(content.getBytes());
-	}
-
-	private Resource createResource(String content) {
-		return new ByteArrayResource(content.getBytes());
-	}
-
-	private static String loadJson(String path) {
-		try {
-			ClassPathResource resource = new ClassPathResource(path, JsonContentAssertTests.class);
-			return new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
-		}
-		catch (Exception ex) {
-			throw new IllegalStateException(ex);
-		}
-
-	}
-
-	private AssertProvider forJson(@Nullable String json) {
-		return () -> new JsonContentAssert(json, JsonContentAssertTests.class);
-	}
-
-}
diff --git a/spring-test/src/test/java/org/springframework/test/json/JsonPathAssertTests.java b/spring-test/src/test/java/org/springframework/test/json/JsonPathAssertTests.java
deleted file mode 100644
index d1a89686283..00000000000
--- a/spring-test/src/test/java/org/springframework/test/json/JsonPathAssertTests.java
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * Copyright 2002-2024 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.test.json;
-
-import java.util.List;
-import java.util.Map;
-import java.util.function.Consumer;
-import java.util.stream.Stream;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.assertj.core.api.AssertProvider;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-import org.junit.jupiter.params.provider.ValueSource;
-
-import org.springframework.core.ParameterizedTypeReference;
-import org.springframework.core.io.ClassPathResource;
-import org.springframework.http.converter.GenericHttpMessageConverter;
-import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
-import org.springframework.lang.Nullable;
-import org.springframework.util.FileCopyUtils;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
-import static org.assertj.core.api.Assertions.entry;
-
-/**
- * Tests for {@link JsonPathAssert}.
- *
- * @author Phillip Webb
- * @author Stephane Nicoll
- */
-class JsonPathAssertTests {
-
-	private static final String TYPES = loadJson("types.json");
-
-	private static final String SIMPSONS = loadJson("simpsons.json");
-
-	private static final String NULLS = loadJson("nulls.json");
-
-	private static final MappingJackson2HttpMessageConverter jsonHttpMessageConverter =
-			new MappingJackson2HttpMessageConverter(new ObjectMapper());
-
-
-	@Nested
-	class HasPathTests {
-
-		@Test
-		void hasPathForPresentAndNotNull() {
-			assertThat(forJson(NULLS)).hasPath("$.valuename");
-		}
-
-		@Test
-		void hasPathForPresentAndNull() {
-			assertThat(forJson(NULLS)).hasPath("$.nullname");
-		}
-
-		@Test
-		void hasPathForOperatorMatching() {
-			assertThat(forJson(SIMPSONS)).
-					hasPath("$.familyMembers[?(@.name == 'Homer')]");
-		}
-
-		@Test
-		void hasPathForOperatorNotMatching() {
-			assertThat(forJson(SIMPSONS)).
-					hasPath("$.familyMembers[?(@.name == 'Dilbert')]");
-		}
-
-		@Test
-		void hasPathForNotPresent() {
-			String expression = "$.missing";
-			assertThatExceptionOfType(AssertionError.class)
-					.isThrownBy(() -> assertThat(forJson(NULLS)).hasPath(expression))
-					.satisfies(hasFailedToMatchPath("$.missing"));
-		}
-
-		@Test
-		void hasPathSatisfying() {
-			assertThat(forJson(TYPES)).hasPathSatisfying("$.str", value -> assertThat(value).isEqualTo("foo"))
-					.hasPathSatisfying("$.num", value -> assertThat(value).isEqualTo(5));
-		}
-
-		@Test
-		void hasPathSatisfyingForPathNotPresent() {
-			String expression = "missing";
-			assertThatExceptionOfType(AssertionError.class)
-					.isThrownBy(() -> assertThat(forJson(NULLS)).hasPathSatisfying(expression, value -> {}))
-					.satisfies(hasFailedToMatchPath(expression));
-		}
-
-		@Test
-		void doesNotHavePathForMissing() {
-			assertThat(forJson(NULLS)).doesNotHavePath("$.missing");
-		}
-
-
-		@Test
-		void doesNotHavePathForPresent() {
-			String expression = "$.valuename";
-			assertThatExceptionOfType(AssertionError.class)
-					.isThrownBy(() -> assertThat(forJson(NULLS)).doesNotHavePath(expression))
-					.satisfies(hasFailedToNotMatchPath(expression));
-		}
-	}
-
-
-	@Nested
-	class ExtractingPathTests {
-
-		@Test
-		void isNullWithNullPathValue() {
-			assertThat(forJson(NULLS)).extractingPath("$.nullname").isNull();
-		}
-
-		@ParameterizedTest
-		@ValueSource(strings = { "$.str", "$.emptyString", "$.num", "$.bool", "$.arr",
-				"$.emptyArray", "$.colorMap", "$.emptyMap" })
-		void isNotNullWithValue(String path) {
-			assertThat(forJson(TYPES)).extractingPath(path).isNotNull();
-		}
-
-		@ParameterizedTest
-		@MethodSource
-		void isEqualToOnRawValue(String path, Object expected) {
-			assertThat(forJson(TYPES)).extractingPath(path).isEqualTo(expected);
-		}
-
-		static Stream isEqualToOnRawValue() {
-			return Stream.of(
-					Arguments.of("$.str", "foo"),
-					Arguments.of("$.num", 5),
-					Arguments.of("$.bool", true),
-					Arguments.of("$.arr", List.of(42)),
-					Arguments.of("$.colorMap", Map.of("red", "rojo")));
-		}
-
-		@Test
-		void asStringWithActualValue() {
-			assertThat(forJson(TYPES)).extractingPath("@.str").asString().startsWith("f").endsWith("o");
-		}
-
-		@Test
-		void asStringIsEmpty() {
-			assertThat(forJson(TYPES)).extractingPath("@.emptyString").asString().isEmpty();
-		}
-
-		@Test
-		void asNumberWithActualValue() {
-			assertThat(forJson(TYPES)).extractingPath("@.num").asNumber().isEqualTo(5);
-		}
-
-		@Test
-		void asBooleanWithActualValue() {
-			assertThat(forJson(TYPES)).extractingPath("@.bool").asBoolean().isTrue();
-		}
-
-		@Test
-		void asArrayWithActualValue() {
-			assertThat(forJson(TYPES)).extractingPath("@.arr").asArray().containsOnly(42);
-		}
-
-		@Test
-		void asArrayIsEmpty() {
-			assertThat(forJson(TYPES)).extractingPath("@.emptyArray").asArray().isEmpty();
-		}
-
-		@Test
-		void asArrayWithFilterPredicatesMatching() {
-			assertThat(forJson(SIMPSONS))
-					.extractingPath("$.familyMembers[?(@.name == 'Bart')]").asArray().hasSize(1);
-		}
-
-		@Test
-		void asArrayWithFilterPredicatesNotMatching() {
-			assertThat(forJson(SIMPSONS)).
-					extractingPath("$.familyMembers[?(@.name == 'Dilbert')]").asArray().isEmpty();
-		}
-
-		@Test
-		void asMapWithActualValue() {
-			assertThat(forJson(TYPES)).extractingPath("@.colorMap").asMap().containsOnly(entry("red", "rojo"));
-		}
-
-		@Test
-		void asMapIsEmpty() {
-			assertThat(forJson(TYPES)).extractingPath("@.emptyMap").asMap().isEmpty();
-		}
-
-		@Test
-		void convertToWithoutHttpMessageConverterShouldFail() {
-			JsonPathValueAssert path = assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[0]");
-			assertThatIllegalStateException()
-					.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(Member.class)
-					.satisfies(member -> assertThat(member.name).isEqualTo("Homer"));
-		}
-
-		@Test
-		void convertToIncompatibleTargetTypeShouldFail() {
-			JsonPathValueAssert path = assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
-					.extractingPath("$.familyMembers[0]");
-			assertThatExceptionOfType(AssertionError.class)
-					.isThrownBy(() -> path.convertTo(Customer.class))
-					.withMessageContainingAll("Expected value at JSON path \"$.familyMembers[0]\":",
-							Customer.class.getName(), "name");
-		}
-
-		@Test
-		void convertArrayToParameterizedType() {
-			assertThat(forJson(SIMPSONS, jsonHttpMessageConverter))
-					.extractingPath("$.familyMembers")
-					.convertTo(new ParameterizedTypeReference>() {})
-					.satisfies(family -> assertThat(family).hasSize(5).element(0).isEqualTo(new Member("Homer")));
-		}
-
-		@Test
-		void isEmptyWithPathHavingNullValue() {
-			assertThat(forJson(NULLS)).extractingPath("nullname").isEmpty();
-		}
-
-		@ParameterizedTest
-		@ValueSource(strings = { "$.emptyString", "$.emptyArray", "$.emptyMap" })
-		void isEmptyWithEmptyValue(String path) {
-			assertThat(forJson(TYPES)).extractingPath(path).isEmpty();
-		}
-
-		@Test
-		void isEmptyForPathWithFilterMatching() {
-			String expression = "$.familyMembers[?(@.name == 'Bart')]";
-			assertThatExceptionOfType(AssertionError.class)
-					.isThrownBy(() -> assertThat(forJson(SIMPSONS)).extractingPath(expression).isEmpty())
-					.withMessageContainingAll("Expected value at JSON path \"" + expression + "\"",
-							"[{\"name\":\"Bart\"}]", "To be empty");
-		}
-
-		@Test
-		void isEmptyForPathWithFilterNotMatching() {
-			assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[?(@.name == 'Dilbert')]").isEmpty();
-		}
-
-		@ParameterizedTest
-		@ValueSource(strings = { "$.str", "$.num", "$.bool", "$.arr", "$.colorMap" })
-		void isNotEmptyWithNonNullValue(String path) {
-			assertThat(forJson(TYPES)).extractingPath(path).isNotEmpty();
-		}
-
-		@Test
-		void isNotEmptyForPathWithFilterMatching() {
-			assertThat(forJson(SIMPSONS)).extractingPath("$.familyMembers[?(@.name == 'Bart')]").isNotEmpty();
-		}
-
-		@Test
-		void isNotEmptyForPathWithFilterNotMatching() {
-			String expression = "$.familyMembers[?(@.name == 'Dilbert')]";
-			assertThatExceptionOfType(AssertionError.class)
-					.isThrownBy(() -> assertThat(forJson(SIMPSONS)).extractingPath(expression).isNotEmpty())
-					.withMessageContainingAll("Expected value at JSON path \"" + expression + "\"",
-							"To not be empty");
-		}
-
-
-		private record Member(String name) {}
-
-		private record Customer(long id, String username) {}
-
-	}
-
-	private Consumer hasFailedToMatchPath(String expression) {
-		return error -> assertThat(error.getMessage()).containsSubsequence("Expecting:",
-				"To match JSON path:", "\"" + expression + "\"");
-	}
-
-	private Consumer hasFailedToNotMatchPath(String expression) {
-		return error -> assertThat(error.getMessage()).containsSubsequence("Expecting:",
-				"Not to match JSON path:", "\"" + expression + "\"");
-	}
-
-
-	private static String loadJson(String path) {
-		try {
-			ClassPathResource resource = new ClassPathResource(path, JsonPathAssertTests.class);
-			return new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
-		}
-		catch (Exception ex) {
-			throw new IllegalStateException(ex);
-		}
-	}
-
-	private AssertProvider forJson(String json) {
-		return forJson(json, null);
-	}
-
-	private AssertProvider forJson(String json,
-			@Nullable GenericHttpMessageConverter jsonHttpMessageConverter) {
-		return () -> new JsonPathAssert(json, jsonHttpMessageConverter);
-	}
-
-}
diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java
index c86ae122d79..4baa08b154e 100644
--- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java
+++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java
@@ -31,7 +31,7 @@ import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
 import org.springframework.mock.web.MockServletContext;
-import org.springframework.test.json.JsonPathAssert;
+import org.springframework.test.json.AbstractJsonContentAssert;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -99,9 +99,9 @@ class AssertableMockMvcTests {
 	@Test
 	void createWithControllersHasNoHttpMessageConverter() {
 		AssertableMockMvc mockMvc = AssertableMockMvc.of(new HelloController());
-		JsonPathAssert jsonPathAssert = assertThat(mockMvc.perform(get("/json"))).hasStatusOk().body().jsonPath();
+		AbstractJsonContentAssert jsonContentAssert = assertThat(mockMvc.perform(get("/json"))).hasStatusOk().body().jsonPath();
 		assertThatIllegalStateException()
-				.isThrownBy(() -> jsonPathAssert.extractingPath("$").convertTo(Message.class))
+				.isThrownBy(() -> jsonContentAssert.extractingPath("$").convertTo(Message.class))
 				.withMessageContaining("No JSON message converter available");
 	}
 

From 5567d14700e9b5ad0caf6e093deca06c90fa2b7d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= 
Date: Mon, 6 May 2024 15:11:30 +0200
Subject: [PATCH 2/4] Move response body directly in
 AbstractMockHttpServletResponseAssert

This commit removes ResponseBodyAssert and rather offers first-class
access support for the response body at the root level using bodyText(),
bodyJson(), and body().

This avoids a double navigation to assert the response body.

See gh-32712
---
 .../test/json/AbstractJsonContentAssert.java  |  49 +++++--
 .../test/json/JsonContent.java                |   2 +-
 .../test/json/JsonContentAssert.java          |  14 +-
 ...AbstractMockHttpServletResponseAssert.java |  77 +++++++++--
 .../servlet/assertj/ResponseBodyAssert.java   | 130 ------------------
 .../json/AbstractJsonContentAssertTests.java  |  28 ++--
 ...actMockHttpServletResponseAssertTests.java |  80 ++++++++---
 .../AssertableMockMvcIntegrationTests.java    |   6 +-
 .../assertj/AssertableMockMvcTests.java       |  22 +--
 .../assertj/ResponseBodyAssertTests.java      |  88 ------------
 10 files changed, 195 insertions(+), 301 deletions(-)
 delete mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java
 delete mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java

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 45b7e839209..4c1771de2cb 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
@@ -52,7 +52,10 @@ import org.springframework.util.function.ThrowingBiFunction;
  * assertions} on the value.
  *
  * 

Also support comparing the JSON document against a target, using - * {@linkplain JSONCompare JSON Assert}. + * {@linkplain JSONCompare JSON Assert}. Resources that are loaded from + * the classpath can be relative if a {@linkplain #withResourceLoadClass(Class) + * class} is provided. By default, {@code UTF-8} is used to load resources + * but this can be overridden using {@link #withCharset(Charset)}. * * @author Stephane Nicoll * @author Phillip Webb @@ -71,28 +74,27 @@ public abstract class AbstractJsonContentAssert jsonMessageConverter; - private final JsonLoader jsonLoader; + @Nullable + private Class resourceLoadClass; + + @Nullable + private Charset charset; + + private JsonLoader jsonLoader; /** * Create an assert for the given JSON document. *

Path can be converted to a value object using the given * {@linkplain GenericHttpMessageConverter json message converter}. - *

Resources to match can be loaded relative to the given - * {@code resourceLoadClass}. If not specified, resources must always be - * absolute. A specific {@link Charset} can be provided if {@code UTF-8} is - * not suitable. * @param json the JSON document to assert * @param jsonMessageConverter the converter to use - * @param resourceLoadClass the class used to load resources - * @param charset the charset of the JSON resources * @param selfType the implementation type of this assert */ protected AbstractJsonContentAssert(@Nullable String json, - @Nullable GenericHttpMessageConverter jsonMessageConverter, @Nullable Class resourceLoadClass, - @Nullable Charset charset, Class selfType) { + @Nullable GenericHttpMessageConverter jsonMessageConverter, Class selfType) { super(json, selfType); this.jsonMessageConverter = jsonMessageConverter; - this.jsonLoader = new JsonLoader(resourceLoadClass, charset); + this.jsonLoader = new JsonLoader(null, null); as("JSON content"); } @@ -376,6 +378,31 @@ public abstract class AbstractJsonContentAssert resourceLoadClass) { + this.resourceLoadClass = resourceLoadClass; + this.jsonLoader = new JsonLoader(resourceLoadClass, this.charset); + return this.myself; + } + + /** + * Override the {@link Charset} to use to load resources. By default, + * resources are loaded using {@code UTF-8}. + * @param charset the charset to use, or {@code null} to use the default + */ + public SELF withCharset(@Nullable Charset charset) { + this.charset = charset; + this.jsonLoader = new JsonLoader(this.resourceLoadClass, charset); + return this.myself; + } + private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONCompareMode compareMode) { return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) -> 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 f36e7ddd7f5..c801a5fa5d6 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 @@ -55,7 +55,7 @@ public final class JsonContent implements AssertProvider { */ @Override public JsonContentAssert assertThat() { - return new JsonContentAssert(this.json, null, this.resourceLoadClass, null); + return new JsonContentAssert(this.json, null).withResourceLoadClass(this.resourceLoadClass); } /** 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 70534f7065f..db0212feeb1 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,8 +16,6 @@ package org.springframework.test.json; -import java.nio.charset.Charset; - import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.lang.Nullable; @@ -33,19 +31,11 @@ public class JsonContentAssert extends AbstractJsonContentAssertPath can be converted to a value object using the given * {@linkplain GenericHttpMessageConverter json message converter}. - *

Resources to match can be loaded relative to the given - * {@code resourceLoadClass}. If not specified, resources must always be - * absolute. A specific {@link Charset} can be provided if {@code UTF-8} is - * not suitable. * @param json the JSON document to assert * @param jsonMessageConverter the converter to use - * @param resourceLoadClass the class used to load resources - * @param charset the charset of the JSON resources */ - public JsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter jsonMessageConverter, - @Nullable Class resourceLoadClass, @Nullable Charset charset) { - - super(json, jsonMessageConverter, resourceLoadClass, charset, JsonContentAssert.class); + public JsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter jsonMessageConverter) { + super(json, jsonMessageConverter, 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 decb5a21e92..c320bea1012 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 @@ -18,9 +18,16 @@ package org.springframework.test.web.servlet.assertj; import java.nio.charset.Charset; +import org.assertj.core.api.AbstractByteArrayAssert; +import org.assertj.core.api.AbstractStringAssert; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.ByteArrayAssert; + import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.json.AbstractJsonContentAssert; +import org.springframework.test.json.JsonContentAssert; import org.springframework.test.web.UriAssert; /** @@ -45,22 +52,62 @@ public abstract class AbstractMockHttpServletResponseAssertExamples:

 	 * // Check that the response body is equal to "Hello World":
-	 * assertThat(response).body().isEqualTo("Hello World");
-	 *
-	 * // Check that the response body is strictly equal to the content of "test.json":
-	 * assertThat(response).body().json().isStrictlyEqualToJson("test.json");
+	 * assertThat(response).bodyText().isEqualTo("Hello World");
 	 * 
*/ - public ResponseBodyAssert body() { - return new ResponseBodyAssert(getResponse().getContentAsByteArray(), - Charset.forName(getResponse().getCharacterEncoding()), this.jsonMessageConverter); + public AbstractStringAssert bodyText() { + return Assertions.assertThat(readBody()); + } + + /** + * Return a new {@linkplain AbstractJsonContentAssert assertion} object that + * uses the response body converted to text as the object to test. Compared + * to {@link #bodyText()}, the assertion object provides dedicated JSON + * support. + *

Examples:


+	 * // Check that the response body is strictly equal to the content of
+	 * // "/com/acme/sample/person-created.json":
+	 * assertThat(response).bodyJson()
+	 *         .isStrictlyEqualToJson("/com/acme/sample/person-created.json");
+	 *
+	 * // Check that the response is strictly equal to the content of the
+	 * // specified file located in the same package as the PersonController:
+	 * assertThat(response).bodyJson().withResourceLoadClass(PersonController.class)
+	 *         .isStrictlyEqualToJson("person-created.json");
+	 * 
+ * The returned assert object also supports JSON path expressions. + *

Examples:


+	 * // Check that the JSON document does not have an "error" element
+	 * assertThat(response).bodyJson().doesNotHavePath("$.error");
+	 *
+	 * // Check that the JSON document as a top level "message" element
+	 * assertThat(response).bodyJson()
+	 *         .extractingPath("$.message").asString().isEqualTo("hello");
+	 * 
+ */ + public AbstractJsonContentAssert bodyJson() { + return new JsonContentAssert(readBody(), this.jsonMessageConverter); + } + + private String readBody() { + return new String(getResponse().getContentAsByteArray(), + Charset.forName(getResponse().getCharacterEncoding())); + } + + /** + * Return a new {@linkplain AbstractByteArrayAssert assertion} object that + * uses the response body as the object to test. + * @see #bodyText() + * @see #bodyJson() + */ + public AbstractByteArrayAssert body() { + return new ByteArrayAssert(getResponse().getContentAsByteArray()); } /** @@ -89,6 +136,14 @@ public abstract class AbstractMockHttpServletResponseAssert { - - private final Charset characterEncoding; - - @Nullable - private final GenericHttpMessageConverter jsonMessageConverter; - - ResponseBodyAssert(byte[] actual, Charset characterEncoding, - @Nullable GenericHttpMessageConverter jsonMessageConverter) { - - super(actual, ResponseBodyAssert.class); - this.characterEncoding = characterEncoding; - this.jsonMessageConverter = jsonMessageConverter; - as("Response body"); - } - - /** - * Return a new {@linkplain AbstractJsonContentAssert assertion} object that - * provides {@linkplain com.jayway.jsonpath.JsonPath JSON path} assertions on - * the response body. - */ - public AbstractJsonContentAssert jsonPath() { - return new JsonContentAssert(getJson(), this.jsonMessageConverter, null, this.characterEncoding) - .as("JSON body"); - } - - /** - * Return a new {@linkplain AbstractJsonContentAssert assertion} object that - * provides support for {@linkplain org.skyscreamer.jsonassert.JSONCompareMode - * JSON assert} comparisons against expected JSON input which can be loaded - * from the classpath. - *

This method only supports absolute locations for JSON documents loaded - * from the classpath. Consider using {@link #json(Class)} to load JSON - * documents relative to a given class. - *

Example:


-	 * // Check that the response is strictly equal to the content of
-	 * // "/com/acme/web/person/person-created.json":
-	 * assertThat(...).body().json()
-	 *         .isStrictlyEqualToJson("/com/acme/web/person/person-created.json");
-	 * 
- */ - public AbstractJsonContentAssert json() { - return json(null); - } - - /** - * Return a new {@linkplain AbstractJsonContentAssert assertion} object that - * provides support for {@linkplain org.skyscreamer.jsonassert.JSONCompareMode - * JSON assert} comparisons against expected JSON input which can be loaded - * from the classpath. - *

Locations for JSON documents can be absolute using a leading slash, or - * relative to the given {@code resourceLoadClass}. - *

Example:


-	 * // Check that the response is strictly equal to the content of the
-	 * // specified file located in the same package as the PersonController:
-	 * assertThat(...).body().json(PersonController.class)
-	 *         .isStrictlyEqualToJson("person-created.json");
-	 * 
- * @param resourceLoadClass the class used to load relative JSON documents - * @see ClassPathResource#ClassPathResource(String, Class) - */ - public AbstractJsonContentAssert json(@Nullable Class resourceLoadClass) { - return new JsonContentAssert(getJson(), this.jsonMessageConverter, resourceLoadClass, this.characterEncoding) - .as("JSON body"); - } - - /** - * Verify that the response body is equal to the given {@link String}. - *

Converts the actual byte array to a String using the character encoding - * of the {@link HttpServletResponse}. - * @param expected the expected content of the response body - * @see #asString() - */ - public ResponseBodyAssert isEqualTo(String expected) { - asString().isEqualTo(expected); - return this; - } - - /** - * Override that uses the character encoding of the {@link HttpServletResponse} - * to convert the byte[] to a String, rather than the platform's default charset. - */ - @Override - public AbstractStringAssert asString() { - return asString(this.characterEncoding); - } - - private String getJson() { - return new String(this.actual, this.characterEncoding); - } - -} 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 5453aebcda9..14f1ad8dcf2 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 @@ -19,7 +19,6 @@ package org.springframework.test.json; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -146,7 +145,6 @@ class AbstractJsonContentAssertTests { assertThat(forJson(NULLS)).doesNotHavePath("$.missing"); } - @Test void doesNotHavePathForPresent() { String expression = "$.valuename"; @@ -154,7 +152,6 @@ class AbstractJsonContentAssertTests { .isThrownBy(() -> assertThat(forJson(NULLS)).doesNotHavePath(expression)) .satisfies(hasFailedToNotMatchPath(expression)); } - } @Nested @@ -330,13 +327,12 @@ class AbstractJsonContentAssertTests { private record Customer(long id, String username) {} private AssertProvider> forJson(@Nullable String json) { - return () -> new TestJsonContentAssert(json, null, null, null); + return () -> new TestJsonContentAssert(json, null); } private AssertProvider> forJson(@Nullable String json, GenericHttpMessageConverter jsonHttpMessageConverter) { - return () -> new TestJsonContentAssert(json, jsonHttpMessageConverter, null, null); + return () -> new TestJsonContentAssert(json, jsonHttpMessageConverter); } - } @Nested @@ -548,7 +544,6 @@ class AbstractJsonContentAssertTests { .isThrownBy(() -> assertThat(forJson(SOURCE)).isStrictlyEqualTo(expected)); } - @Test void isNotEqualToWhenStringIsMatchingShouldFail() { assertThatExceptionOfType(AssertionError.class) @@ -720,10 +715,19 @@ class AbstractJsonContentAssertTests { assertThat(forJson(SOURCE)).isNotStrictlyEqualTo(expected); } - private AssertProvider> forJson(@Nullable String json) { - return () -> new TestJsonContentAssert(json, null, getClass(), null); + @Test + void withResourceLoadClassShouldAllowToLoadRelativeContent() { + AbstractJsonContentAssert jsonAssert = assertThat(forJson(NULLS)).withResourceLoadClass(String.class); + assertThatIllegalStateException() + .isThrownBy(() -> jsonAssert.isLenientlyEqualTo("nulls.json")) + .withMessage("Unable to load JSON from class path resource [java/lang/nulls.json]"); + + assertThat(forJson(NULLS)).withResourceLoadClass(JsonContent.class).isLenientlyEqualTo("nulls.json"); } + private AssertProvider> forJson(@Nullable String json) { + return () -> new TestJsonContentAssert(json, null).withResourceLoadClass(getClass()); + } } @@ -768,13 +772,13 @@ class AbstractJsonContentAssertTests { } private AssertProvider> forJson(@Nullable String json) { - return () -> new TestJsonContentAssert(json, null, null, null); + return () -> new TestJsonContentAssert(json, null); } private static class TestJsonContentAssert extends AbstractJsonContentAssert { - public TestJsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter jsonMessageConverter, @Nullable Class resourceLoadClass, @Nullable Charset charset) { - super(json, jsonMessageConverter, resourceLoadClass, charset, TestJsonContentAssert.class); + public TestJsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter jsonMessageConverter) { + super(json, jsonMessageConverter, TestJsonContentAssert.class); } } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java index 6c0de703ac1..18f8b711032 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java @@ -16,15 +16,17 @@ package org.springframework.test.web.servlet.assertj; - import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; +import org.assertj.core.api.AssertProvider; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.json.JsonContent; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** @@ -34,12 +36,50 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; */ public class AbstractMockHttpServletResponseAssertTests { + @Test + void bodyText() { + MockHttpServletResponse response = createResponse("OK"); + assertThat(fromResponse(response)).bodyText().isEqualTo("OK"); + } + + @Test + void bodyJsonWithJsonPath() { + MockHttpServletResponse response = createResponse("{\"albumById\": {\"name\": \"Greatest hits\"}}"); + assertThat(fromResponse(response)).bodyJson() + .extractingPath("$.albumById.name").isEqualTo("Greatest hits"); + } + + @Test + void bodyJsonCanLoadResourceRelativeToClass() { + MockHttpServletResponse response = createResponse("{ \"name\" : \"Spring\", \"age\" : 123 }"); + // See org/springframework/test/json/example.json + assertThat(fromResponse(response)).bodyJson().withResourceLoadClass(JsonContent.class) + .isLenientlyEqualTo("example.json"); + } + + @Test + void bodyWithByteArray() throws UnsupportedEncodingException { + byte[] bytes = "OK".getBytes(StandardCharsets.UTF_8); + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter().write("OK"); + response.setContentType(StandardCharsets.UTF_8.name()); + assertThat(fromResponse(response)).body().isEqualTo(bytes); + } + + @Test + void hasBodyTextEqualTo() throws UnsupportedEncodingException { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter().write("OK"); + response.setContentType(StandardCharsets.UTF_8.name()); + assertThat(fromResponse(response)).hasBodyTextEqualTo("OK"); + } + @Test void hasForwardedUrl() { String forwardedUrl = "https://example.com/42"; MockHttpServletResponse response = new MockHttpServletResponse(); response.setForwardedUrl(forwardedUrl); - assertThat(response).hasForwardedUrl(forwardedUrl); + assertThat(fromResponse(response)).hasForwardedUrl(forwardedUrl); } @Test @@ -48,7 +88,7 @@ public class AbstractMockHttpServletResponseAssertTests { MockHttpServletResponse response = new MockHttpServletResponse(); response.setForwardedUrl(forwardedUrl); assertThatExceptionOfType(AssertionError.class) - .isThrownBy(() -> assertThat(response).hasForwardedUrl("another")) + .isThrownBy(() -> assertThat(fromResponse(response)).hasForwardedUrl("another")) .withMessageContainingAll("Forwarded URL", forwardedUrl, "another"); } @@ -57,7 +97,7 @@ public class AbstractMockHttpServletResponseAssertTests { String redirectedUrl = "https://example.com/42"; MockHttpServletResponse response = new MockHttpServletResponse(); response.addHeader(HttpHeaders.LOCATION, redirectedUrl); - assertThat(response).hasRedirectedUrl(redirectedUrl); + assertThat(fromResponse(response)).hasRedirectedUrl(redirectedUrl); } @Test @@ -66,29 +106,25 @@ public class AbstractMockHttpServletResponseAssertTests { MockHttpServletResponse response = new MockHttpServletResponse(); response.addHeader(HttpHeaders.LOCATION, redirectedUrl); assertThatExceptionOfType(AssertionError.class) - .isThrownBy(() -> assertThat(response).hasRedirectedUrl("another")) + .isThrownBy(() -> assertThat(fromResponse(response)).hasRedirectedUrl("another")) .withMessageContainingAll("Redirected URL", redirectedUrl, "another"); } - @Test - void bodyHasContent() throws UnsupportedEncodingException { - MockHttpServletResponse response = new MockHttpServletResponse(); - response.getWriter().write("OK"); - assertThat(response).body().asString().isEqualTo("OK"); + + private MockHttpServletResponse createResponse(String body) { + try { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setContentType(StandardCharsets.UTF_8.name()); + response.getWriter().write(body); + return response; + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException(ex); + } } - @Test - void bodyHasContentWithResponseCharacterEncoding() throws UnsupportedEncodingException { - byte[] bytes = "OK".getBytes(StandardCharsets.UTF_8); - MockHttpServletResponse response = new MockHttpServletResponse(); - response.getWriter().write("OK"); - response.setContentType(StandardCharsets.UTF_8.name()); - assertThat(response).body().isEqualTo(bytes); - } - - - private static ResponseAssert assertThat(MockHttpServletResponse response) { - return new ResponseAssert(response); + private static AssertProvider fromResponse(MockHttpServletResponse response) { + return () -> new ResponseAssert(response); } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcIntegrationTests.java index 8e6f3eb7359..34a164f8878 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcIntegrationTests.java @@ -250,19 +250,19 @@ public class AssertableMockMvcIntegrationTests { @Test void jsonPathContent() { - assertThat(perform(get("/message"))).body().jsonPath() + assertThat(perform(get("/message"))).bodyJson() .extractingPath("$.message").asString().isEqualTo("hello"); } @Test void jsonContentCanLoadResourceFromClasspath() { - assertThat(perform(get("/message"))).body().json().isLenientlyEqualTo( + assertThat(perform(get("/message"))).bodyJson().isLenientlyEqualTo( new ClassPathResource("message.json", AssertableMockMvcIntegrationTests.class)); } @Test void jsonContentUsingResourceLoaderClass() { - assertThat(perform(get("/message"))).body().json(AssertableMockMvcIntegrationTests.class) + assertThat(perform(get("/message"))).bodyJson().withResourceLoadClass(AssertableMockMvcIntegrationTests.class) .isLenientlyEqualTo("message.json"); } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java index 4baa08b154e..587f0f521ca 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java @@ -67,26 +67,26 @@ class AssertableMockMvcTests { void createWithExistingWebApplicationContext() { try (GenericWebApplicationContext wac = create(WebConfiguration.class)) { AssertableMockMvc mockMvc = AssertableMockMvc.from(wac); - assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 41"); - assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 42"); + assertThat(mockMvc.perform(post("/increase"))).hasBodyTextEqualTo("counter 41"); + assertThat(mockMvc.perform(post("/increase"))).hasBodyTextEqualTo("counter 42"); } } @Test void createWithControllerClassShouldInstantiateControllers() { AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class, CounterController.class); - assertThat(mockMvc.perform(get("/hello"))).body().isEqualTo("Hello World"); - assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 1"); - assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 2"); + assertThat(mockMvc.perform(get("/hello"))).hasBodyTextEqualTo("Hello World"); + assertThat(mockMvc.perform(post("/increase"))).hasBodyTextEqualTo("counter 1"); + assertThat(mockMvc.perform(post("/increase"))).hasBodyTextEqualTo("counter 2"); } @Test void createWithControllersShouldUseThemAsIs() { AssertableMockMvc mockMvc = AssertableMockMvc.of(new HelloController(), new CounterController(new AtomicInteger(41))); - assertThat(mockMvc.perform(get("/hello"))).body().isEqualTo("Hello World"); - assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 42"); - assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 43"); + assertThat(mockMvc.perform(get("/hello"))).hasBodyTextEqualTo("Hello World"); + assertThat(mockMvc.perform(post("/increase"))).hasBodyTextEqualTo("counter 42"); + assertThat(mockMvc.perform(post("/increase"))).hasBodyTextEqualTo("counter 43"); } @Test @@ -99,7 +99,7 @@ class AssertableMockMvcTests { @Test void createWithControllersHasNoHttpMessageConverter() { AssertableMockMvc mockMvc = AssertableMockMvc.of(new HelloController()); - AbstractJsonContentAssert jsonContentAssert = assertThat(mockMvc.perform(get("/json"))).hasStatusOk().body().jsonPath(); + AbstractJsonContentAssert jsonContentAssert = assertThat(mockMvc.perform(get("/json"))).hasStatusOk().bodyJson(); assertThatIllegalStateException() .isThrownBy(() -> jsonContentAssert.extractingPath("$").convertTo(Message.class)) .withMessageContaining("No JSON message converter available"); @@ -109,7 +109,7 @@ class AssertableMockMvcTests { void createWithControllerCanConfigureHttpMessageConverters() { AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class) .withHttpMessageConverters(List.of(jsonHttpMessageConverter)); - assertThat(mockMvc.perform(get("/json"))).hasStatusOk().body().jsonPath() + assertThat(mockMvc.perform(get("/json"))).hasStatusOk().bodyJson() .extractingPath("$").convertTo(Message.class).satisfies(message -> { assertThat(message.message()).isEqualTo("Hello World"); assertThat(message.counter()).isEqualTo(42); @@ -122,7 +122,7 @@ class AssertableMockMvcTests { MappingJackson2HttpMessageConverter converter = spy(jsonHttpMessageConverter); AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class) .withHttpMessageConverters(List.of(mock(), mock(), converter)); - assertThat(mockMvc.perform(get("/json"))).hasStatusOk().body().jsonPath() + assertThat(mockMvc.perform(get("/json"))).hasStatusOk().bodyJson() .extractingPath("$").convertTo(Message.class).satisfies(message -> { assertThat(message.message()).isEqualTo("Hello World"); assertThat(message.counter()).isEqualTo(42); diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java deleted file mode 100644 index 0284636c3d0..00000000000 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.test.web.servlet.assertj; - - -import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; - -import org.assertj.core.api.AssertProvider; -import org.junit.jupiter.api.Test; - -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.test.json.JsonContent; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ResponseBodyAssert}. - * - * @author Brian Clozel - * @author Stephane Nicoll - */ -class ResponseBodyAssertTests { - - @Test - void isEqualToWithByteArray() { - MockHttpServletResponse response = createResponse("hello"); - response.setCharacterEncoding(StandardCharsets.UTF_8.name()); - assertThat(fromResponse(response)).isEqualTo("hello".getBytes(StandardCharsets.UTF_8)); - } - - @Test - void isEqualToWithString() { - MockHttpServletResponse response = createResponse("hello"); - assertThat(fromResponse(response)).isEqualTo("hello"); - } - - @Test - void jsonPathWithJsonResponseShouldPass() { - MockHttpServletResponse response = createResponse("{\"message\": \"hello\"}"); - assertThat(fromResponse(response)).jsonPath().extractingPath("$.message").isEqualTo("hello"); - } - - @Test - void jsonPathWithJsonCompatibleResponseShouldPass() { - MockHttpServletResponse response = createResponse("{\"albumById\": {\"name\": \"Greatest hits\"}}"); - assertThat(fromResponse(response)).jsonPath() - .extractingPath("$.albumById.name").isEqualTo("Greatest hits"); - } - - @Test - void jsonCanLoadResourceRelativeToClass() { - MockHttpServletResponse response = createResponse("{ \"name\" : \"Spring\", \"age\" : 123 }"); - // See org/springframework/test/json/example.json - assertThat(fromResponse(response)).json(JsonContent.class).isLenientlyEqualTo("example.json"); - } - - private MockHttpServletResponse createResponse(String body) { - try { - MockHttpServletResponse response = new MockHttpServletResponse(); - response.getWriter().print(body); - return response; - } - catch (UnsupportedEncodingException ex) { - throw new IllegalStateException(ex); - } - } - - private AssertProvider fromResponse(MockHttpServletResponse response) { - return () -> new ResponseBodyAssert(response.getContentAsByteArray(), Charset.forName(response.getCharacterEncoding()), null); - } - -} From c8967de065603e6e515a08406fc9f46a82f7468f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 6 May 2024 17:21:30 +0200 Subject: [PATCH 3/4] Provide better name for entry points This commit renames AssertableMockMvc to MockMvcTester as the former was too mouthful, and we are looking for a naming pattern that can be applied consistently to test utilities that rely on AssertJ. Rather than AssertableMvcResult, we now use MvcTestResult and it no longer extends from MvcResult itself. That avoids having existing code that is migrated to the new API whilst assigning the result to MvcResult and get a bad DevXP as that doesn't bring any of the AssertJ support. See gh-32712 --- ...cResult.java => DefaultMvcTestResult.java} | 79 ++++------------ ...ertableMockMvc.java => MockMvcTester.java} | 89 +++++++++++++------ ...tableMvcResult.java => MvcTestResult.java} | 25 ++++-- ...ltAssert.java => MvcTestResultAssert.java} | 54 +++++------ ...ts.java => DefaultMvcTestResultTests.java} | 47 ++-------- ...ava => MockMvcTesterIntegrationTests.java} | 27 +++--- ...kMvcTests.java => MockMvcTesterTests.java} | 26 +++--- 7 files changed, 155 insertions(+), 192 deletions(-) rename spring-test/src/main/java/org/springframework/test/web/servlet/assertj/{DefaultAssertableMvcResult.java => DefaultMvcTestResult.java} (53%) rename spring-test/src/main/java/org/springframework/test/web/servlet/assertj/{AssertableMockMvc.java => MockMvcTester.java} (70%) rename spring-test/src/main/java/org/springframework/test/web/servlet/assertj/{AssertableMvcResult.java => MvcTestResult.java} (61%) rename spring-test/src/main/java/org/springframework/test/web/servlet/assertj/{MvcResultAssert.java => MvcTestResultAssert.java} (82%) rename spring-test/src/test/java/org/springframework/test/web/servlet/assertj/{DefaultAssertableMvcResultTests.java => DefaultMvcTestResultTests.java} (57%) rename spring-test/src/test/java/org/springframework/test/web/servlet/assertj/{AssertableMockMvcIntegrationTests.java => MockMvcTesterIntegrationTests.java} (94%) rename spring-test/src/test/java/org/springframework/test/web/servlet/assertj/{AssertableMockMvcTests.java => MockMvcTesterTests.java} (88%) diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResult.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultMvcTestResult.java similarity index 53% rename from spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResult.java rename to spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultMvcTestResult.java index 83f4037c3ae..8c718080692 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultMvcTestResult.java @@ -21,20 +21,17 @@ import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.web.servlet.MvcResult; -import org.springframework.web.servlet.FlashMap; -import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.ModelAndView; /** - * The default {@link AssertableMvcResult} implementation. + * The default {@link MvcTestResult} implementation. * * @author Stephane Nicoll * @since 6.2 */ -final class DefaultAssertableMvcResult implements AssertableMvcResult { +final class DefaultMvcTestResult implements MvcTestResult { @Nullable - private final MvcResult target; + private final MvcResult mvcResult; @Nullable private final Exception unresolvedException; @@ -42,86 +39,46 @@ final class DefaultAssertableMvcResult implements AssertableMvcResult { @Nullable private final GenericHttpMessageConverter jsonMessageConverter; - DefaultAssertableMvcResult(@Nullable MvcResult target, @Nullable Exception unresolvedException, @Nullable GenericHttpMessageConverter jsonMessageConverter) { - this.target = target; + DefaultMvcTestResult(@Nullable MvcResult mvcResult, @Nullable Exception unresolvedException, @Nullable GenericHttpMessageConverter jsonMessageConverter) { + this.mvcResult = mvcResult; this.unresolvedException = unresolvedException; this.jsonMessageConverter = jsonMessageConverter; } - /** - * Return the exception that was thrown unexpectedly while processing the - * request, if any. - */ + public MvcResult getMvcResult() { + if (this.mvcResult == null) { + throw new IllegalStateException( + "Request has failed with unresolved exception " + this.unresolvedException); + } + return this.mvcResult; + } + @Nullable public Exception getUnresolvedException() { return this.unresolvedException; } - @Override public MockHttpServletRequest getRequest() { - return getTarget().getRequest(); + return getMvcResult().getRequest(); } - @Override public MockHttpServletResponse getResponse() { - return getTarget().getResponse(); + return getMvcResult().getResponse(); } - @Override - @Nullable - public Object getHandler() { - return getTarget().getHandler(); - } - - @Override - @Nullable - public HandlerInterceptor[] getInterceptors() { - return getTarget().getInterceptors(); - } - - @Override - @Nullable - public ModelAndView getModelAndView() { - return getTarget().getModelAndView(); - } - - @Override @Nullable public Exception getResolvedException() { - return getTarget().getResolvedException(); + return getMvcResult().getResolvedException(); } - @Override - public FlashMap getFlashMap() { - return getTarget().getFlashMap(); - } - - @Override - public Object getAsyncResult() { - return getTarget().getAsyncResult(); - } - - @Override - public Object getAsyncResult(long timeToWait) { - return getTarget().getAsyncResult(timeToWait); - } - - - private MvcResult getTarget() { - if (this.target == null) { - throw new IllegalStateException( - "Request has failed with unresolved exception " + this.unresolvedException); - } - return this.target; - } /** * Use AssertJ's {@link org.assertj.core.api.Assertions#assertThat assertThat} * instead. */ @Override - public MvcResultAssert assertThat() { - return new MvcResultAssert(this, this.jsonMessageConverter); + public MvcTestResultAssert assertThat() { + return new MvcTestResultAssert(this, this.jsonMessageConverter); } } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMockMvc.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java similarity index 70% rename from spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMockMvc.java rename to spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java index 6e99119b9a9..cca3087f602 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMockMvc.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java @@ -38,15 +38,46 @@ import org.springframework.util.Assert; import org.springframework.web.context.WebApplicationContext; /** - * {@link MockMvc} variant that tests Spring MVC exchanges and provides fluent - * assertions using {@link org.assertj.core.api.Assertions AssertJ}. + * Test Spring MVC applications with {@link MockMvc} for server request handling + * using {@link org.assertj.core.api.Assertions AssertJ}. + * + *

A tester instance can be created from a {@link WebApplicationContext}: + *


+ * // Create an instance with default settings
+ * MockMvcTester mvc = MockMvcTester.from(applicationContext);
+ *
+ * // Create an instance with a custom Filter
+ * MockMvcTester mvc = MockMvcTester.from(applicationContext,
+ *         builder -> builder.addFilters(filter).build());
+ * 
+ * + *

A tester can be created standalone by providing the controller(s) to + * include in a standalone setup:


+ * // Create an instance for PersonController
+ * MockMvcTester mvc = MockMvcTester.of(new PersonController());
+ * 
+ * + *

Once a test instance is available, you can perform requests in + * a similar fashion as with {@link MockMvc}, and wrapping the result in + * {@code assertThat} provides access to assertions. For instance: + *


+ * // perform a GET on /hi and assert the response body is equal to Hello
+ * assertThat(mvc.perform(get("/hi")))
+ *         .hasStatusOk().hasBodyTextEqualTo("Hello");
+ * 
* *

A main difference with {@link MockMvc} is that an unresolved exception - * is not thrown directly. Rather an {@link AssertableMvcResult} is available - * with an {@link AssertableMvcResult#getUnresolvedException() unresolved - * exception}. + * is not thrown directly. Rather an {@link MvcTestResult} is available + * with an {@link MvcTestResult#getUnresolvedException() unresolved + * exception}. You can assert that a request has failed unexpectedly: + *


+ * // perform a GET on /hi and assert the response body is equal to Hello
+ * assertThat(mvc.perform(get("/boom")))
+ *         .hasUnresolvedException())
+ * 		   .withMessage("Test exception");
+ * 
* - *

{@link AssertableMockMvc} can be configured with a list of + *

{@link MockMvcTester} can be configured with a list of * {@linkplain HttpMessageConverter message converters} to allow the response * body to be deserialized, rather than asserting on the raw values. * @@ -54,7 +85,7 @@ import org.springframework.web.context.WebApplicationContext; * @author Brian Clozel * @since 6.2 */ -public final class AssertableMockMvc { +public final class MockMvcTester { private static final MediaType JSON = MediaType.APPLICATION_JSON; @@ -64,23 +95,23 @@ public final class AssertableMockMvc { private final GenericHttpMessageConverter jsonMessageConverter; - private AssertableMockMvc(MockMvc mockMvc, @Nullable GenericHttpMessageConverter jsonMessageConverter) { + private MockMvcTester(MockMvc mockMvc, @Nullable GenericHttpMessageConverter jsonMessageConverter) { Assert.notNull(mockMvc, "mockMVC should not be null"); this.mockMvc = mockMvc; this.jsonMessageConverter = jsonMessageConverter; } /** - * Create a {@link AssertableMockMvc} instance that delegates to the given + * Create a {@link MockMvcTester} instance that delegates to the given * {@link MockMvc} instance. * @param mockMvc the MockMvc instance to delegate calls to */ - public static AssertableMockMvc create(MockMvc mockMvc) { - return new AssertableMockMvc(mockMvc, null); + public static MockMvcTester create(MockMvc mockMvc) { + return new MockMvcTester(mockMvc, null); } /** - * Create an {@link AssertableMockMvc} instance using the given, fully + * Create an {@link MockMvcTester} instance using the given, fully * initialized (i.e., refreshed) {@link WebApplicationContext}. The * given {@code customizations} are applied to the {@link DefaultMockMvcBuilder} * that ultimately creates the underlying {@link MockMvc} instance. @@ -92,7 +123,7 @@ public final class AssertableMockMvc { * instance based on a {@link DefaultMockMvcBuilder}. * @see MockMvcBuilders#webAppContextSetup(WebApplicationContext) */ - public static AssertableMockMvc from(WebApplicationContext applicationContext, + public static MockMvcTester from(WebApplicationContext applicationContext, Function customizations) { DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(applicationContext); @@ -101,7 +132,7 @@ public final class AssertableMockMvc { } /** - * Shortcut to create an {@link AssertableMockMvc} instance using the given, + * Shortcut to create an {@link MockMvcTester} instance using the given, * fully initialized (i.e., refreshed) {@link WebApplicationContext}. *

Consider using {@link #from(WebApplicationContext, Function)} if * further customization of the underlying {@link MockMvc} instance is @@ -110,12 +141,12 @@ public final class AssertableMockMvc { * MVC infrastructure and application controllers from * @see MockMvcBuilders#webAppContextSetup(WebApplicationContext) */ - public static AssertableMockMvc from(WebApplicationContext applicationContext) { + public static MockMvcTester from(WebApplicationContext applicationContext) { return from(applicationContext, DefaultMockMvcBuilder::build); } /** - * Create an {@link AssertableMockMvc} instance by registering one or more + * Create an {@link MockMvcTester} instance by registering one or more * {@code @Controller} instances and configuring Spring MVC infrastructure * programmatically. *

This allows full control over the instantiation and initialization of @@ -129,7 +160,7 @@ public final class AssertableMockMvc { * Spring MVC infrastructure * @see MockMvcBuilders#standaloneSetup(Object...) */ - public static AssertableMockMvc of(Collection controllers, + public static MockMvcTester of(Collection controllers, Function customizations) { StandaloneMockMvcBuilder builder = MockMvcBuilders.standaloneSetup(controllers.toArray()); @@ -137,8 +168,8 @@ public final class AssertableMockMvc { } /** - * Shortcut to create an {@link AssertableMockMvc} instance by registering - * one or more {@code @Controller} instances. + * Shortcut to create an {@link MockMvcTester} instance by registering one + * or more {@code @Controller} instances. *

The minimum infrastructure required by the * {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlet} * to serve requests with annotated controllers is created. Consider using @@ -149,12 +180,12 @@ public final class AssertableMockMvc { * into an instance * @see MockMvcBuilders#standaloneSetup(Object...) */ - public static AssertableMockMvc of(Object... controllers) { + public static MockMvcTester of(Object... controllers) { return of(Arrays.asList(controllers), StandaloneMockMvcBuilder::build); } /** - * Return a new {@link AssertableMockMvc} instance using the specified + * Return a new {@link MockMvcTester} instance using the specified * {@linkplain HttpMessageConverter message converters}. *

If none are specified, only basic assertions on the response body can * be performed. Consider registering a suitable JSON converter for asserting @@ -162,13 +193,13 @@ public final class AssertableMockMvc { * @param httpMessageConverters the message converters to use * @return a new instance using the specified converters */ - public AssertableMockMvc withHttpMessageConverters(Iterable> httpMessageConverters) { - return new AssertableMockMvc(this.mockMvc, findJsonMessageConverter(httpMessageConverters)); + public MockMvcTester withHttpMessageConverters(Iterable> httpMessageConverters) { + return new MockMvcTester(this.mockMvc, findJsonMessageConverter(httpMessageConverters)); } /** - * Perform a request and return a type that can be used with standard - * {@link org.assertj.core.api.Assertions AssertJ} assertions. + * Perform a request and return a {@link MvcTestResult result} that can be + * used with standard {@link org.assertj.core.api.Assertions AssertJ} assertions. *

Use static methods of {@link MockMvcRequestBuilders} to prepare the * request, wrapping the invocation in {@code assertThat}. The following * asserts that a {@linkplain MockMvcRequestBuilders#get(URI) GET} request @@ -191,16 +222,16 @@ public final class AssertableMockMvc { * @param requestBuilder used to prepare the request to execute; * see static factory methods in * {@link org.springframework.test.web.servlet.request.MockMvcRequestBuilders} - * @return an {@link AssertableMvcResult} to be wrapped in {@code assertThat} + * @return an {@link MvcTestResult} to be wrapped in {@code assertThat} * @see MockMvc#perform(RequestBuilder) */ - public AssertableMvcResult perform(RequestBuilder requestBuilder) { + public MvcTestResult perform(RequestBuilder requestBuilder) { Object result = getMvcResultOrFailure(requestBuilder); if (result instanceof MvcResult mvcResult) { - return new DefaultAssertableMvcResult(mvcResult, null, this.jsonMessageConverter); + return new DefaultMvcTestResult(mvcResult, null, this.jsonMessageConverter); } else { - return new DefaultAssertableMvcResult(null, (Exception) result, this.jsonMessageConverter); + return new DefaultMvcTestResult(null, (Exception) result, this.jsonMessageConverter); } } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMvcResult.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResult.java similarity index 61% rename from spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMvcResult.java rename to spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResult.java index 8f0674db61d..db8745bc174 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMvcResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResult.java @@ -22,23 +22,34 @@ import org.springframework.lang.Nullable; import org.springframework.test.web.servlet.MvcResult; /** - * A {@link MvcResult} that additionally supports AssertJ style assertions. + * Provide to the result of an executed request using {@link MockMvcTester} that + * is meant to be used with {@link org.assertj.core.api.Assertions#assertThat(AssertProvider) + * assertThat}. * *

Can be in one of two distinct states: *

    - *
  1. The request processed successfully, and {@link #getUnresolvedException()} - * is therefore {@code null}.
  2. + *
  3. The request processed successfully, even if it fails with an exception + * that has been resolved. {@link #getMvcResult()} is available and + * {@link #getUnresolvedException()} is {@code null}.
  4. *
  5. The request failed unexpectedly with {@link #getUnresolvedException()} - * providing more information about the error. Any attempt to access a member of - * the result fails with an exception.
  6. + * providing more information about the error. Any attempt to access + * {@link #getMvcResult() the result } fails with an exception. *
* * @author Stephane Nicoll * @author Brian Clozel * @since 6.2 - * @see AssertableMockMvc + * @see MockMvcTester */ -public interface AssertableMvcResult extends MvcResult, AssertProvider { +public interface MvcTestResult extends AssertProvider { + + /** + * Return the {@link MvcResult result} of the processing. + *

If the request has failed unexpectedly, this throws an + * {@link IllegalStateException}. + * @return the {@link MvcResult} + */ + MvcResult getMvcResult(); /** * Return the exception that was thrown unexpectedly while processing the diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcResultAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java similarity index 82% rename from spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcResultAssert.java rename to spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java index e4e666546f9..ca1dd65c4ac 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcResultAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java @@ -43,22 +43,22 @@ import org.springframework.web.servlet.ModelAndView; /** * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied - * to {@link MvcResult}. + * to {@link MvcTestResult}. * * @author Stephane Nicoll * @author Brian Clozel * @since 6.2 */ -public class MvcResultAssert extends AbstractMockHttpServletResponseAssert { +public class MvcTestResultAssert extends AbstractMockHttpServletResponseAssert { - MvcResultAssert(AssertableMvcResult mvcResult, @Nullable GenericHttpMessageConverter jsonMessageConverter) { - super(jsonMessageConverter, mvcResult, MvcResultAssert.class); + MvcTestResultAssert(MvcTestResult actual, @Nullable GenericHttpMessageConverter jsonMessageConverter) { + super(jsonMessageConverter, actual, MvcTestResultAssert.class); } @Override protected MockHttpServletResponse getResponse() { - checkHasNotFailedUnexpectedly(); - return this.actual.getResponse(); + getMvcResult(); + return this.actual.getMvcResult().getResponse(); } /** @@ -76,8 +76,7 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert request() { - checkHasNotFailedUnexpectedly(); - return new MockHttpRequestAssert(this.actual.getRequest()); + return new MockHttpRequestAssert(getMvcResult().getRequest()); } /** @@ -85,8 +84,7 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert */ public HandlerResultAssert handler() { - checkHasNotFailedUnexpectedly(); - return new HandlerResultAssert(this.actual.getHandler()); + return new HandlerResultAssert(getMvcResult().getHandler()); } /** @@ -118,7 +114,6 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert viewName() { - checkHasNotFailedUnexpectedly(); return Assertions.assertThat(getModelAndView().getViewName()).as("View name"); } @@ -139,8 +133,7 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert flash() { - checkHasNotFailedUnexpectedly(); - return new MapAssert<>(this.actual.getFlashMap()); + return new MapAssert<>(getMvcResult().getFlashMap()); } /** @@ -151,14 +144,14 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert asyncResult() { request().hasAsyncStarted(true); - return Assertions.assertThat(this.actual.getAsyncResult()).as("Async result"); + return Assertions.assertThat(getMvcResult().getAsyncResult()).as("Async result"); } /** * Verify that the request has failed with an unresolved exception. * @see #unresolvedException() */ - public MvcResultAssert hasUnresolvedException() { + public MvcTestResultAssert hasUnresolvedException() { Assertions.assertThat(this.actual.getUnresolvedException()) .withFailMessage("Expecting request to have failed but it has succeeded").isNotNull(); return this; @@ -167,7 +160,7 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert resultMatcher.match(mvcResult)); } /** * Apply the given {@link ResultHandler} to the actual mvc result. * @param resultHandler the result matcher to invoke */ - public MvcResultAssert apply(ResultHandler resultHandler) { - checkHasNotFailedUnexpectedly(); - return satisfies(resultHandler::handle); + public MvcTestResultAssert apply(ResultHandler resultHandler) { + MvcResult mvcResult = getMvcResult(); + return satisfies(tmc -> resultHandler.handle(mvcResult)); } /** @@ -197,7 +190,7 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert { diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResultTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/DefaultMvcTestResultTests.java similarity index 57% rename from spring-test/src/test/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResultTests.java rename to spring-test/src/test/java/org/springframework/test/web/servlet/assertj/DefaultMvcTestResultTests.java index f59a53521d9..e1e1867c893 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResultTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/DefaultMvcTestResultTests.java @@ -30,76 +30,47 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; /** - * Tests for {@link DefaultAssertableMvcResult}. + * Tests for {@link DefaultMvcTestResult}. * * @author Stephane Nicoll */ -class DefaultAssertableMvcResultTests { +class DefaultMvcTestResultTests { @Test void createWithMvcResultDelegatesToIt() { MockHttpServletRequest request = new MockHttpServletRequest(); MvcResult mvcResult = mock(MvcResult.class); given(mvcResult.getRequest()).willReturn(request); - DefaultAssertableMvcResult result = new DefaultAssertableMvcResult(mvcResult, null, null); + DefaultMvcTestResult result = new DefaultMvcTestResult(mvcResult, null, null); assertThat(result.getRequest()).isSameAs(request); verify(mvcResult).getRequest(); } @Test void createWithExceptionDoesNotAllowAccessToRequest() { - assertRequestHasFailed(DefaultAssertableMvcResult::getRequest); + assertRequestHasFailed(DefaultMvcTestResult::getRequest); } @Test void createWithExceptionDoesNotAllowAccessToResponse() { - assertRequestHasFailed(DefaultAssertableMvcResult::getResponse); + assertRequestHasFailed(DefaultMvcTestResult::getResponse); } - @Test - void createWithExceptionDoesNotAllowAccessToHandler() { - assertRequestHasFailed(DefaultAssertableMvcResult::getHandler); - } - - @Test - void createWithExceptionDoesNotAllowAccessToInterceptors() { - assertRequestHasFailed(DefaultAssertableMvcResult::getInterceptors); - } - - @Test - void createWithExceptionDoesNotAllowAccessToModelAndView() { - assertRequestHasFailed(DefaultAssertableMvcResult::getModelAndView); - } @Test void createWithExceptionDoesNotAllowAccessToResolvedException() { - assertRequestHasFailed(DefaultAssertableMvcResult::getResolvedException); - } - - @Test - void createWithExceptionDoesNotAllowAccessToFlashMap() { - assertRequestHasFailed(DefaultAssertableMvcResult::getFlashMap); - } - - @Test - void createWithExceptionDoesNotAllowAccessToAsyncResult() { - assertRequestHasFailed(DefaultAssertableMvcResult::getAsyncResult); - } - - @Test - void createWithExceptionDoesNotAllowAccessToAsyncResultWithTimeToWait() { - assertRequestHasFailed(result -> result.getAsyncResult(1000)); + assertRequestHasFailed(DefaultMvcTestResult::getResolvedException); } @Test void createWithExceptionReturnsException() { IllegalStateException exception = new IllegalStateException("Expected"); - DefaultAssertableMvcResult result = new DefaultAssertableMvcResult(null, exception, null); + DefaultMvcTestResult result = new DefaultMvcTestResult(null, exception, null); assertThat(result.getUnresolvedException()).isSameAs(exception); } - private void assertRequestHasFailed(Consumer action) { - DefaultAssertableMvcResult result = new DefaultAssertableMvcResult(null, new IllegalStateException("Expected"), null); + private void assertRequestHasFailed(Consumer action) { + DefaultMvcTestResult result = new DefaultMvcTestResult(null, new IllegalStateException("Expected"), null); assertThatIllegalStateException().isThrownBy(() -> action.accept(result)) .withMessageContaining("Request has failed with unresolved exception"); } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java similarity index 94% rename from spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcIntegrationTests.java rename to spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java index 34a164f8878..4d58ad5c37a 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java @@ -43,7 +43,6 @@ import org.springframework.stereotype.Controller; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.Person; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.ui.Model; import org.springframework.validation.Errors; @@ -69,19 +68,19 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** - * Integration tests for {@link AssertableMockMvc}. + * Integration tests for {@link MockMvcTester}. * * @author Brian Clozel * @author Stephane Nicoll */ @SpringJUnitConfig @WebAppConfiguration -public class AssertableMockMvcIntegrationTests { +public class MockMvcTesterIntegrationTests { - private final AssertableMockMvc mockMvc; + private final MockMvcTester mockMvc; - AssertableMockMvcIntegrationTests(WebApplicationContext wac) { - this.mockMvc = AssertableMockMvc.from(wac); + MockMvcTesterIntegrationTests(WebApplicationContext wac) { + this.mockMvc = MockMvcTester.from(wac); } @Nested @@ -126,8 +125,8 @@ public class AssertableMockMvcIntegrationTests { assertThat(performWithCookie(cookie, get("/greet"))).cookies().hasValue("test", "value"); } - private AssertableMvcResult performWithCookie(Cookie cookie, MockHttpServletRequestBuilder request) { - AssertableMockMvc mockMvc = AssertableMockMvc.of(List.of(new TestController()), builder -> builder.addInterceptors( + private MvcTestResult performWithCookie(Cookie cookie, MockHttpServletRequestBuilder request) { + MockMvcTester mockMvc = MockMvcTester.of(List.of(new TestController()), builder -> builder.addInterceptors( new HandlerInterceptor() { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { @@ -257,12 +256,12 @@ public class AssertableMockMvcIntegrationTests { @Test void jsonContentCanLoadResourceFromClasspath() { assertThat(perform(get("/message"))).bodyJson().isLenientlyEqualTo( - new ClassPathResource("message.json", AssertableMockMvcIntegrationTests.class)); + new ClassPathResource("message.json", MockMvcTesterIntegrationTests.class)); } @Test void jsonContentUsingResourceLoaderClass() { - assertThat(perform(get("/message"))).bodyJson().withResourceLoadClass(AssertableMockMvcIntegrationTests.class) + assertThat(perform(get("/message"))).bodyJson().withResourceLoadClass(MockMvcTesterIntegrationTests.class) .isLenientlyEqualTo("message.json"); } @@ -416,8 +415,8 @@ public class AssertableMockMvcIntegrationTests { } - private void testAssertionFailureWithUnresolvableException(Consumer assertions) { - AssertableMvcResult result = perform(get("/error/1")); + private void testAssertionFailureWithUnresolvableException(Consumer assertions) { + MvcTestResult result = perform(get("/error/1")); assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertions.accept(result)) .withMessageContainingAll("Request has failed unexpectedly:", @@ -441,7 +440,7 @@ public class AssertableMockMvcIntegrationTests { @Test void satisfiesAllowsAdditionalAssertions() { assertThat(this.mockMvc.perform(get("/greet"))).satisfies(result -> { - assertThat(result).isInstanceOf(MvcResult.class); + assertThat(result).isInstanceOf(MvcTestResult.class); assertThat(result).hasStatusOk(); }); } @@ -467,7 +466,7 @@ public class AssertableMockMvcIntegrationTests { } - private AssertableMvcResult perform(MockHttpServletRequestBuilder builder) { + private MvcTestResult perform(MockHttpServletRequestBuilder builder) { return this.mockMvc.perform(builder); } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterTests.java similarity index 88% rename from spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java rename to spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterTests.java index 587f0f521ca..a8f17e6cf62 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterTests.java @@ -48,11 +48,11 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; /** - * Tests for {@link AssertableMockMvc}. + * Tests for {@link MockMvcTester}. * * @author Stephane Nicoll */ -class AssertableMockMvcTests { +class MockMvcTesterTests { private static final MappingJackson2HttpMessageConverter jsonHttpMessageConverter = new MappingJackson2HttpMessageConverter(new ObjectMapper()); @@ -60,13 +60,13 @@ class AssertableMockMvcTests { @Test void createShouldRejectNullMockMvc() { - assertThatIllegalArgumentException().isThrownBy(() -> AssertableMockMvc.create(null)); + assertThatIllegalArgumentException().isThrownBy(() -> MockMvcTester.create(null)); } @Test void createWithExistingWebApplicationContext() { try (GenericWebApplicationContext wac = create(WebConfiguration.class)) { - AssertableMockMvc mockMvc = AssertableMockMvc.from(wac); + MockMvcTester mockMvc = MockMvcTester.from(wac); assertThat(mockMvc.perform(post("/increase"))).hasBodyTextEqualTo("counter 41"); assertThat(mockMvc.perform(post("/increase"))).hasBodyTextEqualTo("counter 42"); } @@ -74,7 +74,7 @@ class AssertableMockMvcTests { @Test void createWithControllerClassShouldInstantiateControllers() { - AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class, CounterController.class); + MockMvcTester mockMvc = MockMvcTester.of(HelloController.class, CounterController.class); assertThat(mockMvc.perform(get("/hello"))).hasBodyTextEqualTo("Hello World"); assertThat(mockMvc.perform(post("/increase"))).hasBodyTextEqualTo("counter 1"); assertThat(mockMvc.perform(post("/increase"))).hasBodyTextEqualTo("counter 2"); @@ -82,7 +82,7 @@ class AssertableMockMvcTests { @Test void createWithControllersShouldUseThemAsIs() { - AssertableMockMvc mockMvc = AssertableMockMvc.of(new HelloController(), + MockMvcTester mockMvc = MockMvcTester.of(new HelloController(), new CounterController(new AtomicInteger(41))); assertThat(mockMvc.perform(get("/hello"))).hasBodyTextEqualTo("Hello World"); assertThat(mockMvc.perform(post("/increase"))).hasBodyTextEqualTo("counter 42"); @@ -91,14 +91,14 @@ class AssertableMockMvcTests { @Test void createWithControllerAndCustomizations() { - AssertableMockMvc mockMvc = AssertableMockMvc.of(List.of(new HelloController()), builder -> + MockMvcTester mockMvc = MockMvcTester.of(List.of(new HelloController()), builder -> builder.defaultRequest(get("/hello").accept(MediaType.APPLICATION_JSON)).build()); assertThat(mockMvc.perform(get("/hello"))).hasStatus(HttpStatus.NOT_ACCEPTABLE); } @Test void createWithControllersHasNoHttpMessageConverter() { - AssertableMockMvc mockMvc = AssertableMockMvc.of(new HelloController()); + MockMvcTester mockMvc = MockMvcTester.of(new HelloController()); AbstractJsonContentAssert jsonContentAssert = assertThat(mockMvc.perform(get("/json"))).hasStatusOk().bodyJson(); assertThatIllegalStateException() .isThrownBy(() -> jsonContentAssert.extractingPath("$").convertTo(Message.class)) @@ -107,7 +107,7 @@ class AssertableMockMvcTests { @Test void createWithControllerCanConfigureHttpMessageConverters() { - AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class) + MockMvcTester mockMvc = MockMvcTester.of(HelloController.class) .withHttpMessageConverters(List.of(jsonHttpMessageConverter)); assertThat(mockMvc.perform(get("/json"))).hasStatusOk().bodyJson() .extractingPath("$").convertTo(Message.class).satisfies(message -> { @@ -120,7 +120,7 @@ class AssertableMockMvcTests { @SuppressWarnings("unchecked") void withHttpMessageConverterDetectsJsonConverter() { MappingJackson2HttpMessageConverter converter = spy(jsonHttpMessageConverter); - AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class) + MockMvcTester mockMvc = MockMvcTester.of(HelloController.class) .withHttpMessageConverters(List.of(mock(), mock(), converter)); assertThat(mockMvc.perform(get("/json"))).hasStatusOk().bodyJson() .extractingPath("$").convertTo(Message.class).satisfies(message -> { @@ -132,11 +132,11 @@ class AssertableMockMvcTests { @Test void performWithUnresolvedExceptionSetsException() { - AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class); - AssertableMvcResult result = mockMvc.perform(get("/error")); + MockMvcTester mockMvc = MockMvcTester.of(HelloController.class); + MvcTestResult result = mockMvc.perform(get("/error")); assertThat(result.getUnresolvedException()).isInstanceOf(ServletException.class) .cause().isInstanceOf(IllegalStateException.class).hasMessage("Expected"); - assertThat(result).hasFieldOrPropertyWithValue("target", null); + assertThat(result).hasFieldOrPropertyWithValue("mvcResult", null); } private GenericWebApplicationContext create(Class... classes) { From bcecce7aace32538badce20b9ee8b0d03da12114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 6 May 2024 17:33:30 +0200 Subject: [PATCH 4/4] Add shortcuts for frequently used assertions See gh-32712 --- .../AbstractHttpServletResponseAssert.java | 105 ++++++++++++++++-- .../servlet/assertj/MvcTestResultAssert.java | 10 -- ...bstractHttpServletResponseAssertTests.java | 59 +++++++++- .../MockMvcTesterIntegrationTests.java | 14 +-- 4 files changed, 157 insertions(+), 31 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java index ddba8a187f6..fe079f7a9d8 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java @@ -28,7 +28,9 @@ import org.assertj.core.api.Assertions; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus.Series; +import org.springframework.http.MediaType; import org.springframework.test.http.HttpHeadersAssert; +import org.springframework.test.http.MediaTypeAssert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.function.SingletonSupplier; @@ -48,15 +50,24 @@ import org.springframework.util.function.SingletonSupplier; public abstract class AbstractHttpServletResponseAssert, ACTUAL> extends AbstractObjectAssert { - private final Supplier> statusAssert; + private final Supplier contentTypeAssertSupplier; private final Supplier headersAssertSupplier; + private final Supplier> statusAssert; + protected AbstractHttpServletResponseAssert(ACTUAL actual, Class selfType) { super(actual, selfType); - this.statusAssert = SingletonSupplier.of(() -> Assertions.assertThat(getResponse().getStatus()).as("HTTP status code")); + this.contentTypeAssertSupplier = SingletonSupplier.of(() -> new MediaTypeAssert(getResponse().getContentType())); this.headersAssertSupplier = SingletonSupplier.of(() -> new HttpHeadersAssert(getHttpHeaders(getResponse()))); + this.statusAssert = SingletonSupplier.of(() -> Assertions.assertThat(getResponse().getStatus()).as("HTTP status code")); + } + + private static HttpHeaders getHttpHeaders(HttpServletResponse response) { + MultiValueMap headers = new LinkedMultiValueMap<>(); + response.getHeaderNames().forEach(name -> headers.put(name, new ArrayList<>(response.getHeaders(name)))); + return new HttpHeaders(headers); } /** @@ -67,6 +78,14 @@ public abstract class AbstractHttpServletResponseAssert headers = new LinkedMultiValueMap<>(); - response.getHeaderNames().forEach(name -> headers.put(name, new ArrayList<>(response.getHeaders(name)))); - return new HttpHeaders(headers); - } - } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java index ca1dd65c4ac..7c7ff332ec8 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java @@ -30,12 +30,10 @@ import org.assertj.core.api.ObjectAssert; import org.assertj.core.error.BasicErrorMessageFactory; import org.assertj.core.internal.Failures; -import org.springframework.http.MediaType; import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.test.http.MediaTypeAssert; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; import org.springframework.test.web.servlet.ResultMatcher; @@ -87,14 +85,6 @@ public class MvcTestResultAssert extends AbstractMockHttpServletResponseAssert headers) { MockHttpServletResponse response = new MockHttpServletResponse(); headers.forEach(response::addHeader); @@ -51,6 +69,45 @@ class AbstractHttpServletResponseAssertTests { } } + @Nested + class ContentTypeTests { + + @Test + void contentType() { + MockHttpServletResponse response = createResponse("text/plain"); + assertThat(response).hasContentType(MediaType.TEXT_PLAIN); + } + + @Test + void contentTypeAndRepresentation() { + MockHttpServletResponse response = createResponse("text/plain"); + assertThat(response).hasContentType("text/plain"); + } + + @Test + void contentTypeCompatibleWith() { + MockHttpServletResponse response = createResponse("application/json;charset=UTF-8"); + assertThat(response).hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON); + } + + @Test + void contentTypeCompatibleWithAndStringRepresentation() { + MockHttpServletResponse response = createResponse("text/plain"); + assertThat(response).hasContentTypeCompatibleWith("text/*"); + } + + @Test + void contentTypeCanBeAsserted() { + MockHttpServletResponse response = createResponse("text/plain"); + assertThat(response).contentType().isInstanceOf(MediaType.class).isCompatibleWith("text/*").isNotNull(); + } + + private MockHttpServletResponse createResponse(String contentType) { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setContentType(contentType); + return response; + } + } @Nested class StatusTests { diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java index 4d58ad5c37a..1a6f69daa3f 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java @@ -138,16 +138,6 @@ public class MockMvcTesterIntegrationTests { } } - @Nested - class ContentTypeTests { - - @Test - void contentType() { - assertThat(perform(get("/greet"))).contentType().isCompatibleWith("text/plain"); - } - - } - @Nested class StatusTests { @@ -168,8 +158,8 @@ public class MockMvcTesterIntegrationTests { @Test void shouldAssertHeader() { - assertThat(perform(get("/greet"))).headers() - .hasValue("Content-Type", "text/plain;charset=ISO-8859-1"); + assertThat(perform(get("/greet"))) + .hasHeader("Content-Type", "text/plain;charset=ISO-8859-1"); } @Test