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 0000000000..4c1771de2c --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java @@ -0,0 +1,528 @@ +/* + * 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}. 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 + * @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; + + @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}. + * @param json the JSON document to assert + * @param jsonMessageConverter the converter to use + * @param selfType the implementation type of this assert + */ + protected AbstractJsonContentAssert(@Nullable String json, + @Nullable GenericHttpMessageConverter jsonMessageConverter, Class selfType) { + super(json, selfType); + this.jsonMessageConverter = jsonMessageConverter; + this.jsonLoader = new JsonLoader(null, null); + 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); + } + + /** + * Override the class used to load resources. Resources can be loaded from + * an absolute location or relative to tpe specified class. For instance, + * specifying {@code com.example.MyClass} as the resource class allows you + * to use "my-file.json" to load {@code /com/example/my-file.json}. + * @param resourceLoadClass the class used to load resources or {@code null} + * to only use absolute paths. + */ + public SELF withResourceLoadClass(@Nullable Class 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) -> + 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 a81c4724e1..c801a5fa5d 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).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 391c466c3f..db0212feeb 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,26 @@ 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 - * @param resourceLoadClass the class used to load resources - * @param charset the charset of the JSON resources + * Create an assert for the given JSON document. + *

Path can be converted to a value object using the given + * {@linkplain GenericHttpMessageConverter json message converter}. + * @param json the JSON document to assert + * @param jsonMessageConverter the converter to use */ - public JsonContentAssert(@Nullable CharSequence json, @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; + public JsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter jsonMessageConverter) { + super(json, jsonMessageConverter, 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 2ba28fcafd..0000000000 --- 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/AbstractHttpServletResponseAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java index ddba8a187f..fe079f7a9d 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/AbstractMockHttpServletResponseAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java index decb5a21e9..c320bea101 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 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 6e99119b9a..cca3087f60 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 8f0674db61..db8745bc17 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 78% 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 e4e666546f..7c7ff332ec 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 @@ -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; @@ -43,22 +41,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 +74,7 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert request() { - checkHasNotFailedUnexpectedly(); - return new MockHttpRequestAssert(this.actual.getRequest()); + return new MockHttpRequestAssert(getMvcResult().getRequest()); } /** @@ -85,17 +82,7 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert */ public HandlerResultAssert handler() { - checkHasNotFailedUnexpectedly(); - return new HandlerResultAssert(this.actual.getHandler()); + return new HandlerResultAssert(getMvcResult().getHandler()); } /** @@ -118,7 +104,6 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert viewName() { - checkHasNotFailedUnexpectedly(); return Assertions.assertThat(getModelAndView().getViewName()).as("View name"); } @@ -139,8 +123,7 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert flash() { - checkHasNotFailedUnexpectedly(); - return new MapAssert<>(this.actual.getFlashMap()); + return new MapAssert<>(getMvcResult().getFlashMap()); } /** @@ -151,14 +134,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 +150,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 +180,7 @@ public class MvcResultAssert extends AbstractMockHttpServletResponseAssert { 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 deleted file mode 100644 index fffd483150..0000000000 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java +++ /dev/null @@ -1,128 +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.nio.charset.Charset; - -import jakarta.servlet.http.HttpServletResponse; -import org.assertj.core.api.AbstractByteArrayAssert; -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.JsonContentAssert; -import org.springframework.test.json.JsonPathAssert; - -/** - * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to - * the response body. - * - * @author Stephane Nicoll - * @author Brian Clozel - * @since 6.2 - */ -public class ResponseBodyAssert extends AbstractByteArrayAssert { - - 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 JsonPathAssert assertion} object that provides - * {@linkplain com.jayway.jsonpath.JsonPath JSON path} assertions on the - * response body. - */ - public JsonPathAssert jsonPath() { - return new JsonPathAssert(getJson(), this.jsonMessageConverter); - } - - /** - * 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. - *

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 JsonContentAssert 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. - *

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 JsonContentAssert json(@Nullable Class resourceLoadClass) { - return new JsonContentAssert(getJson(), resourceLoadClass, this.characterEncoding); - } - - /** - * 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 new file mode 100644 index 0000000000..14f1ad8dcf --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/json/AbstractJsonContentAssertTests.java @@ -0,0 +1,785 @@ +/* + * 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.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); + } + + private AssertProvider> forJson(@Nullable String json, GenericHttpMessageConverter jsonHttpMessageConverter) { + return () -> new TestJsonContentAssert(json, jsonHttpMessageConverter); + } + } + + @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); + } + + @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()); + } + } + + + 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); + } + + private static class TestJsonContentAssert extends AbstractJsonContentAssert { + + public TestJsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter jsonMessageConverter) { + super(json, jsonMessageConverter, 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 02c839bd8e..0000000000 --- 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 d1a8968628..0000000000 --- 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/AbstractHttpServletResponseAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssertTests.java index 0d28a4ea93..1344ada365 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssertTests.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletResponse; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -37,13 +38,30 @@ class AbstractHttpServletResponseAssertTests { @Nested class HeadersTests { + @Test + void containsHeader() { + MockHttpServletResponse response = createResponse(Map.of("n1", "v1", "n2", "v2", "n3", "v3")); + assertThat(response).containsHeader("n1"); + } + + @Test + void doesNotContainHeader() { + MockHttpServletResponse response = createResponse(Map.of("n1", "v1", "n2", "v2", "n3", "v3")); + assertThat(response).doesNotContainHeader("n4"); + } + + @Test + void hasHeader() { + MockHttpServletResponse response = createResponse(Map.of("n1", "v1", "n2", "v2", "n3", "v3")); + assertThat(response).hasHeader("n1", "v1"); + } + @Test void headersAreMatching() { MockHttpServletResponse response = createResponse(Map.of("n1", "v1", "n2", "v2", "n3", "v3")); assertThat(response).headers().containsHeaders("n1", "n2", "n3"); } - private MockHttpServletResponse createResponse(Map 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/AbstractMockHttpServletResponseAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java index 6c0de703ac..18f8b71103 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/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 f59a53521d..e1e1867c89 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 91% 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 8e6f3eb735..1a6f69daa3 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) { @@ -139,16 +138,6 @@ public class AssertableMockMvcIntegrationTests { } } - @Nested - class ContentTypeTests { - - @Test - void contentType() { - assertThat(perform(get("/greet"))).contentType().isCompatibleWith("text/plain"); - } - - } - @Nested class StatusTests { @@ -169,8 +158,8 @@ public class AssertableMockMvcIntegrationTests { @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 @@ -250,19 +239,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( - new ClassPathResource("message.json", AssertableMockMvcIntegrationTests.class)); + assertThat(perform(get("/message"))).bodyJson().isLenientlyEqualTo( + new ClassPathResource("message.json", MockMvcTesterIntegrationTests.class)); } @Test void jsonContentUsingResourceLoaderClass() { - assertThat(perform(get("/message"))).body().json(AssertableMockMvcIntegrationTests.class) + assertThat(perform(get("/message"))).bodyJson().withResourceLoadClass(MockMvcTesterIntegrationTests.class) .isLenientlyEqualTo("message.json"); } @@ -416,8 +405,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 +430,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 +456,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 74% 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 c86ae122d7..a8f17e6cf6 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 @@ -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; @@ -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,56 +60,56 @@ 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); - assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 41"); - assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 42"); + MockMvcTester mockMvc = MockMvcTester.from(wac); + 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"); + 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"); } @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"))).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 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()); - JsonPathAssert jsonPathAssert = assertThat(mockMvc.perform(get("/json"))).hasStatusOk().body().jsonPath(); + MockMvcTester mockMvc = MockMvcTester.of(new HelloController()); + AbstractJsonContentAssert jsonContentAssert = assertThat(mockMvc.perform(get("/json"))).hasStatusOk().bodyJson(); assertThatIllegalStateException() - .isThrownBy(() -> jsonPathAssert.extractingPath("$").convertTo(Message.class)) + .isThrownBy(() -> jsonContentAssert.extractingPath("$").convertTo(Message.class)) .withMessageContaining("No JSON message converter available"); } @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().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); @@ -120,9 +120,9 @@ 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().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); @@ -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) { 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 0284636c3d..0000000000 --- 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); - } - -}