Browse Source

Add AssertJ support for MockMvc

Closes gh-21178
pull/32467/head
Stéphane Nicoll 2 years ago
parent
commit
4a74e1fc2d
  1. 1
      spring-test/spring-test.gradle
  2. 129
      spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java
  3. 107
      spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java
  4. 9
      spring-test/src/main/java/org/springframework/test/http/package-info.java
  5. 235
      spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java
  6. 73
      spring-test/src/main/java/org/springframework/test/json/JsonContent.java
  7. 367
      spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java
  8. 74
      spring-test/src/main/java/org/springframework/test/json/JsonLoader.java
  9. 165
      spring-test/src/main/java/org/springframework/test/json/JsonPathAssert.java
  10. 48
      spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java
  11. 9
      spring-test/src/main/java/org/springframework/test/json/package-info.java
  12. 62
      spring-test/src/main/java/org/springframework/test/util/MethodAssert.java
  13. 123
      spring-test/src/main/java/org/springframework/test/validation/AbstractBindingResultAssert.java
  14. 9
      spring-test/src/main/java/org/springframework/test/validation/package-info.java
  15. 101
      spring-test/src/main/java/org/springframework/test/web/UriAssert.java
  16. 122
      spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java
  17. 167
      spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java
  18. 38
      spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssert.java
  19. 109
      spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java
  20. 227
      spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMockMvc.java
  21. 50
      spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMvcResult.java
  22. 173
      spring-test/src/main/java/org/springframework/test/web/servlet/assertj/CookieMapAssert.java
  23. 123
      spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResult.java
  24. 120
      spring-test/src/main/java/org/springframework/test/web/servlet/assertj/HandlerResultAssert.java
  25. 163
      spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java
  26. 258
      spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcResultAssert.java
  27. 125
      spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java
  28. 9
      spring-test/src/main/java/org/springframework/test/web/servlet/assertj/package-info.java
  29. 190
      spring-test/src/test/java/org/springframework/test/http/HttpHeadersAssertTests.java
  30. 157
      spring-test/src/test/java/org/springframework/test/http/MediaTypeAssertTests.java
  31. 479
      spring-test/src/test/java/org/springframework/test/json/JsonContentAssertTests.java
  32. 60
      spring-test/src/test/java/org/springframework/test/json/JsonContentTests.java
  33. 322
      spring-test/src/test/java/org/springframework/test/json/JsonPathAssertTests.java
  34. 333
      spring-test/src/test/java/org/springframework/test/json/JsonPathValueAssertTests.java
  35. 92
      spring-test/src/test/java/org/springframework/test/util/MethodAssertTests.java
  36. 136
      spring-test/src/test/java/org/springframework/test/validation/AbstractBindingResultAssertTests.java
  37. 77
      spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java
  38. 143
      spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssertTests.java
  39. 138
      spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssertTests.java
  40. 48
      spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssertTests.java
  41. 106
      spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java
  42. 558
      spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcIntegrationTests.java
  43. 210
      spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java
  44. 182
      spring-test/src/test/java/org/springframework/test/web/servlet/assertj/CookieMapAssertTests.java
  45. 107
      spring-test/src/test/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResultTests.java
  46. 142
      spring-test/src/test/java/org/springframework/test/web/servlet/assertj/HandlerResultAssertTests.java
  47. 176
      spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ModelAssertTests.java
  48. 88
      spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java
  49. 6
      spring-test/src/test/resources/org/springframework/test/json/different.json
  50. 4
      spring-test/src/test/resources/org/springframework/test/json/example.json
  51. 6
      spring-test/src/test/resources/org/springframework/test/json/lenient-same.json
  52. 4
      spring-test/src/test/resources/org/springframework/test/json/nulls.json
  53. 36
      spring-test/src/test/resources/org/springframework/test/json/simpsons.json
  54. 6
      spring-test/src/test/resources/org/springframework/test/json/source.json
  55. 18
      spring-test/src/test/resources/org/springframework/test/json/types.json
  56. 3
      spring-test/src/test/resources/org/springframework/test/web/servlet/assertj/message.json

1
spring-test/spring-test.gradle

@ -32,6 +32,7 @@ dependencies { @@ -32,6 +32,7 @@ dependencies {
optional("org.apache.groovy:groovy")
optional("org.apache.tomcat.embed:tomcat-embed-core")
optional("org.aspectj:aspectjweaver")
optional("org.assertj:assertj-core")
optional("org.hamcrest:hamcrest")
optional("org.htmlunit:htmlunit") {
exclude group: "commons-logging", module: "commons-logging"

129
spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java

@ -0,0 +1,129 @@ @@ -0,0 +1,129 @@
/*
* 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.http;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import org.assertj.core.api.AbstractMapAssert;
import org.assertj.core.api.Assertions;
import org.springframework.http.HttpHeaders;
/**
* AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to
* {@link HttpHeaders}.
*
* @author Stephane Nicoll
* @since 6.2
*/
public class HttpHeadersAssert extends AbstractMapAssert<HttpHeadersAssert, HttpHeaders, String, List<String>> {
private static final ZoneId GMT = ZoneId.of("GMT");
public HttpHeadersAssert(HttpHeaders actual) {
super(actual, HttpHeadersAssert.class);
as("HTTP headers");
}
/**
* Verify that the actual HTTP headers contain a header with the given
* {@code name}.
* @param name the name of an expected HTTP header
* @see #containsKey
*/
public HttpHeadersAssert containsHeader(String name) {
return containsKey(name);
}
/**
* Verify that the actual HTTP headers contain the headers with the given
* {@code names}.
* @param names the names of expected HTTP headers
* @see #containsKeys
*/
public HttpHeadersAssert containsHeaders(String... names) {
return containsKeys(names);
}
/**
* Verify that the actual HTTP headers do not contain a header with the
* given {@code name}.
* @param name the name of an HTTP header that should not be present
* @see #doesNotContainKey
*/
public HttpHeadersAssert doesNotContainsHeader(String name) {
return doesNotContainKey(name);
}
/**
* Verify that the actual HTTP headers do not contain any of the headers
* with the given {@code names}.
* @param names the names of HTTP headers that should not be present
* @see #doesNotContainKeys
*/
public HttpHeadersAssert doesNotContainsHeaders(String... names) {
return doesNotContainKeys(names);
}
/**
* Verify that the actual HTTP headers contain a header with the given
* {@code name} and {@link String} {@code value}.
* @param name the name of the cookie
* @param value the expected value of the header
*/
public HttpHeadersAssert hasValue(String name, String value) {
containsKey(name);
Assertions.assertThat(this.actual.getFirst(name))
.as("check primary value for HTTP header '%s'", name)
.isEqualTo(value);
return this.myself;
}
/**
* Verify that the actual HTTP headers contain a header with the given
* {@code name} and {@link Long} {@code value}.
* @param name the name of the cookie
* @param value the expected value of the header
*/
public HttpHeadersAssert hasValue(String name, long value) {
containsKey(name);
Assertions.assertThat(this.actual.getFirst(name))
.as("check primary long value for HTTP header '%s'", name)
.asLong().isEqualTo(value);
return this.myself;
}
/**
* Verify that the actual HTTP headers contain a header with the given
* {@code name} and {@link Instant} {@code value}.
* @param name the name of the cookie
* @param value the expected value of the header
*/
public HttpHeadersAssert hasValue(String name, Instant value) {
containsKey(name);
Assertions.assertThat(this.actual.getFirstZonedDateTime(name))
.as("check primary date value for HTTP header '%s'", name)
.isCloseTo(ZonedDateTime.ofInstant(value, GMT), Assertions.within(999, ChronoUnit.MILLIS));
return this.myself;
}
}

107
spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java

@ -0,0 +1,107 @@ @@ -0,0 +1,107 @@
/*
* 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.http;
import org.assertj.core.api.AbstractObjectAssert;
import org.assertj.core.api.Assertions;
import org.assertj.core.error.BasicErrorMessageFactory;
import org.assertj.core.internal.Failures;
import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied
* to a {@link MediaType}.
*
* @author Brian Clozel
* @author Stephane Nicoll
* @since 6.2
*/
public class MediaTypeAssert extends AbstractObjectAssert<MediaTypeAssert, MediaType> {
public MediaTypeAssert(@Nullable MediaType mediaType) {
super(mediaType, MediaTypeAssert.class);
as("Media type");
}
public MediaTypeAssert(@Nullable String actual) {
this(StringUtils.hasText(actual) ? MediaType.parseMediaType(actual) : null);
}
/**
* Verify that the actual media type is equal to the given string
* representation.
* @param expected the expected media type
*/
public MediaTypeAssert isEqualTo(String expected) {
return isEqualTo(parseMediaType(expected));
}
/**
* Verify that the actual media type is
* {@linkplain MediaType#isCompatibleWith(MediaType) compatible} with the
* given one. Example: <pre><code class='java'>
* // Check that actual is compatible with "application/json"
* assertThat(mediaType).isCompatibleWith(MediaType.APPLICATION_JSON);
* </code></pre>
* @param mediaType the media type with which to compare
*/
public MediaTypeAssert isCompatibleWith(MediaType mediaType) {
Assertions.assertThat(this.actual)
.withFailMessage("Expecting null to be compatible with '%s'", mediaType).isNotNull();
Assertions.assertThat(mediaType)
.withFailMessage("Expecting '%s' to be compatible with null", this.actual).isNotNull();
Assertions.assertThat(this.actual.isCompatibleWith(mediaType))
.as("check media type '%s' is compatible with '%s'", this.actual.toString(), mediaType.toString())
.isTrue();
return this;
}
/**
* Verify that the actual media type is
* {@linkplain MediaType#isCompatibleWith(MediaType) compatible} with the
* given one. Example: <pre><code class='java'>
* // Check that actual is compatible with "text/plain"
* assertThat(mediaType).isCompatibleWith("text/plain");
* </code></pre>
* @param mediaType the media type with which to compare
*/
public MediaTypeAssert isCompatibleWith(String mediaType) {
return isCompatibleWith(parseMediaType(mediaType));
}
private MediaType parseMediaType(String value) {
try {
return MediaType.parseMediaType(value);
}
catch (InvalidMediaTypeException ex) {
throw Failures.instance().failure(this.info, new ShouldBeValidMediaType(value, ex.getMessage()));
}
}
private static final class ShouldBeValidMediaType extends BasicErrorMessageFactory {
private ShouldBeValidMediaType(String mediaType, String errorMessage) {
super("%nExpecting:%n %s%nTo be a valid media type but got:%n %s%n", mediaType, errorMessage);
}
}
}

9
spring-test/src/main/java/org/springframework/test/http/package-info.java

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
/**
* Test support for HTTP concepts.
*/
@NonNullApi
@NonNullFields
package org.springframework.test.http;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

235
spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java

@ -0,0 +1,235 @@ @@ -0,0 +1,235 @@
/*
* 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.lang.reflect.Array;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import org.assertj.core.api.AbstractBooleanAssert;
import org.assertj.core.api.AbstractMapAssert;
import org.assertj.core.api.AbstractObjectAssert;
import org.assertj.core.api.AbstractStringAssert;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.ObjectArrayAssert;
import org.assertj.core.error.BasicErrorMessageFactory;
import org.assertj.core.internal.Failures;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.mock.http.MockHttpInputMessage;
import org.springframework.mock.http.MockHttpOutputMessage;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Base AssertJ {@link org.assertj.core.api.Assert assertions} that can be
* applied to a JSON value. In JSON, values must be one of the following data
* types:
* <ul>
* <li>a {@linkplain #asString() string}</li>
* <li>a {@linkplain #asNumber() number}</li>
* <li>a {@linkplain #asBoolean() boolean}</li>
* <li>an {@linkplain #asArray() array}</li>
* <li>an {@linkplain #asMap() object} (JSON object)</li>
* <li>{@linkplain #isNull() null}</li>
* </ul>
* This base class offers direct access for each of those types as well as a
* conversion methods based on an optional {@link GenericHttpMessageConverter}.
*
* @author Stephane Nicoll
* @since 6.2
* @param <SELF> the type of assertions
*/
public abstract class AbstractJsonValueAssert<SELF extends AbstractJsonValueAssert<SELF>>
extends AbstractObjectAssert<SELF, Object> {
private final Failures failures = Failures.instance();
@Nullable
private final GenericHttpMessageConverter<Object> httpMessageConverter;
protected AbstractJsonValueAssert(@Nullable Object actual, Class<?> selfType,
@Nullable GenericHttpMessageConverter<Object> httpMessageConverter) {
super(actual, selfType);
this.httpMessageConverter = httpMessageConverter;
}
/**
* Verify that the actual value is a non-{@code null} {@link String}
* and return a new {@linkplain AbstractStringAssert assertion} object that
* provides dedicated {@code String} assertions for it.
*/
@Override
public AbstractStringAssert<?> asString() {
return Assertions.assertThat(castTo(String.class, "a string"));
}
/**
* Verify that the actual value is a non-{@code null} {@link Number},
* usually an {@link Integer} or {@link Double} and return a new
* {@linkplain AbstractObjectAssert assertion} object for it.
*/
public AbstractObjectAssert<?, Number> asNumber() {
return Assertions.assertThat(castTo(Number.class, "a number"));
}
/**
* Verify that the actual value is a non-{@code null} {@link Boolean}
* and return a new {@linkplain AbstractBooleanAssert assertion} object
* that provides dedicated {@code Boolean} assertions for it.
*/
public AbstractBooleanAssert<?> asBoolean() {
return Assertions.assertThat(castTo(Boolean.class, "a boolean"));
}
/**
* Verify that the actual value is a non-{@code null} {@link Array}
* and return a new {@linkplain ObjectArrayAssert assertion} object
* that provides dedicated {@code Array} assertions for it.
*/
public ObjectArrayAssert<Object> asArray() {
List<?> list = castTo(List.class, "an array");
Object[] array = list.toArray(new Object[0]);
return Assertions.assertThat(array);
}
/**
* Verify that the actual value is a non-{@code null} JSON object and
* return a new {@linkplain AbstractMapAssert assertion} object that
* provides dedicated assertions on individual elements of the
* object. The returned map assertion object uses the attribute name as the
* key, and the value can itself be any of the valid JSON values.
*/
@SuppressWarnings("unchecked")
public AbstractMapAssert<?, Map<String, Object>, String, Object> asMap() {
return Assertions.assertThat(castTo(Map.class, "a map"));
}
private <T> T castTo(Class<T> expectedType, String description) {
if (this.actual == null) {
throw valueProcessingFailed("To be %s%n".formatted(description));
}
if (!expectedType.isInstance(this.actual)) {
throw valueProcessingFailed("To be %s%nBut was:%n %s%n".formatted(description, this.actual.getClass().getName()));
}
return expectedType.cast(this.actual);
}
/**
* Verify that the actual value can be converted to an instance of the
* given {@code target} and produce a new {@linkplain AbstractObjectAssert
* assertion} object narrowed to that type.
* @param target the {@linkplain Class type} to convert the actual value to
*/
public <T> AbstractObjectAssert<?, T> convertTo(Class<T> target) {
isNotNull();
T value = convertToTargetType(target);
return Assertions.assertThat(value);
}
/**
* Verify that the actual value can be converted to an instance of the
* given {@code target} and produce a new {@linkplain AbstractObjectAssert
* assertion} object narrowed to that type.
* @param target the {@linkplain ParameterizedTypeReference parameterized
* type} to convert the actual value to
*/
public <T> AbstractObjectAssert<?, T> convertTo(ParameterizedTypeReference<T> target) {
isNotNull();
T value = convertToTargetType(target.getType());
return Assertions.assertThat(value);
}
/**
* Verify that the actual value is empty, that is a {@code null} scalar
* value or an empty list or map. Can also be used when the path is using a
* filter operator to validate that it dit not match.
*/
public SELF isEmpty() {
if (!ObjectUtils.isEmpty(this.actual)) {
throw valueProcessingFailed("To be empty");
}
return this.myself;
}
/**
* Verify that the actual value is not empty, that is a non-{@code null}
* scalar value or a non-empty list or map. Can also be used when the path is
* using a filter operator to validate that it dit match at least one
* element.
*/
public SELF isNotEmpty() {
if (ObjectUtils.isEmpty(this.actual)) {
throw valueProcessingFailed("To not be empty");
}
return this.myself;
}
@SuppressWarnings("unchecked")
private <T> T convertToTargetType(Type targetType) {
if (this.httpMessageConverter == null) {
throw new IllegalStateException(
"No JSON message converter available to convert %s".formatted(actualToString()));
}
try {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
this.httpMessageConverter.write(this.actual, ResolvableType.forInstance(this.actual).getType(),
MediaType.APPLICATION_JSON, outputMessage);
return (T) this.httpMessageConverter.read(targetType, getClass(),
fromHttpOutputMessage(outputMessage));
}
catch (Exception ex) {
throw valueProcessingFailed("To convert successfully to:%n %s%nBut it failed:%n %s%n"
.formatted(targetType.getTypeName(), ex.getMessage()));
}
}
private HttpInputMessage fromHttpOutputMessage(MockHttpOutputMessage message) {
MockHttpInputMessage inputMessage = new MockHttpInputMessage(message.getBodyAsBytes());
inputMessage.getHeaders().addAll(message.getHeaders());
return inputMessage;
}
protected String getExpectedErrorMessagePrefix() {
return "Expected:";
}
private AssertionError valueProcessingFailed(String errorMessage) {
throw this.failures.failure(this.info, new ValueProcessingFailed(
getExpectedErrorMessagePrefix(), actualToString(), errorMessage));
}
private String actualToString() {
return ObjectUtils.nullSafeToString(StringUtils.quoteIfString(this.actual));
}
private static final class ValueProcessingFailed extends BasicErrorMessageFactory {
private ValueProcessingFailed(String prefix, String actualToString, String errorMessage) {
super("%n%s%n %s%n%s".formatted(prefix, actualToString, errorMessage));
}
}
}

73
spring-test/src/main/java/org/springframework/test/json/JsonContent.java

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
/*
* 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 org.assertj.core.api.AssertProvider;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* JSON content usually created from a JSON tester. Generally used only to
* {@link AssertProvider provide} {@link JsonContentAssert} to AssertJ
* {@code assertThat} calls.
*
* @author Phillip Webb
* @author Diego Berrueta
* @since 6.2
*/
public final class JsonContent implements AssertProvider<JsonContentAssert> {
private final String json;
@Nullable
private final Class<?> resourceLoadClass;
/**
* Create a new {@link JsonContent} instance.
* @param json the actual JSON content
* @param resourceLoadClass the source class used to load resources
*/
JsonContent(String json, @Nullable Class<?> resourceLoadClass) {
Assert.notNull(json, "JSON must not be null");
this.json = json;
this.resourceLoadClass = resourceLoadClass;
}
/**
* Use AssertJ's {@link org.assertj.core.api.Assertions#assertThat assertThat}
* instead.
*/
@Override
public JsonContentAssert assertThat() {
return new JsonContentAssert(this.json, this.resourceLoadClass, null);
}
/**
* Return the actual JSON content string.
* @return the JSON content
*/
public String getJson() {
return this.json;
}
@Override
public String toString() {
return "JsonContent " + this.json;
}
}

367
spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java

@ -0,0 +1,367 @@ @@ -0,0 +1,367 @@
/*
* 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 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.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}.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Diego Berrueta
* @author Camille Vienot
* @author Stephane Nicoll
* @since 6.2
*/
public class JsonContentAssert extends AbstractAssert<JsonContentAssert, CharSequence> {
private final JsonLoader loader;
/**
* 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 source class used to load resources
* @param charset the charset of the JSON resources
*/
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 source 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}.
* <p>The resource abstraction allows to provide several input types:
* <ul>
* <li>a {@code byte} array, using {@link ByteArrayResource}</li>
* <li>a {@code classpath} resource, using {@link ClassPathResource}</li>
* <li>a {@link File} or {@link Path}, using {@link FileSystemResource}</li>
* <li>an {@link InputStream}, using {@link InputStreamResource}</li>
* </ul>
* @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}.
* <p>The resource abstraction allows to provide several input types:
* <ul>
* <li>a {@code byte} array, using {@link ByteArrayResource}</li>
* <li>a {@code classpath} resource, using {@link ClassPathResource}</li>
* <li>a {@link File} or {@link Path}, using {@link FileSystemResource}</li>
* <li>an {@link InputStream}, using {@link InputStreamResource}</li>
* </ul>
* @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}.
* <p>The resource abstraction allows to provide several input types:
* <ul>
* <li>a {@code byte} array, using {@link ByteArrayResource}</li>
* <li>a {@code classpath} resource, using {@link ClassPathResource}</li>
* <li>a {@link File} or {@link Path}, using {@link FileSystemResource}</li>
* <li>an {@link InputStream}, using {@link InputStreamResource}</li>
* </ul>
* @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}.
* <p>The resource abstraction allows to provide several input types:
* <ul>
* <li>a {@code byte} array, using {@link ByteArrayResource}</li>
* <li>a {@code classpath} resource, using {@link ClassPathResource}</li>
* <li>a {@link File} or {@link Path}, using {@link FileSystemResource}</li>
* <li>an {@link InputStream}, using {@link InputStreamResource}</li>
* </ul>
* @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}.
* <p>The resource abstraction allows to provide several input types:
* <ul>
* <li>a {@code byte} array, using {@link ByteArrayResource}</li>
* <li>a {@code classpath} resource, using {@link ClassPathResource}</li>
* <li>a {@link File} or {@link Path}, using {@link FileSystemResource}</li>
* <li>an {@link InputStream}, using {@link InputStreamResource}</li>
* </ul>
* @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}.
* <p>The resource abstraction allows to provide several input types:
* <ul>
* <li>a {@code byte} array, using {@link ByteArrayResource}</li>
* <li>a {@code classpath} resource, using {@link ClassPathResource}</li>
* <li>a {@link File} or {@link Path}, using {@link FileSystemResource}</li>
* <li>an {@link InputStream}, using {@link InputStreamResource}</li>
* </ul>
* @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}.
* <p>The resource abstraction allows to provide several input types:
* <ul>
* <li>a {@code byte} array, using {@link ByteArrayResource}</li>
* <li>a {@code classpath} resource, using {@link ClassPathResource}</li>
* <li>a {@link File} or {@link Path}, using {@link FileSystemResource}</li>
* <li>an {@link InputStream}, using {@link InputStreamResource}</li>
* </ul>
* @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}.
* <p>The resource abstraction allows to provide several input types:
* <ul>
* <li>a {@code byte} array, using {@link ByteArrayResource}</li>
* <li>a {@code classpath} resource, using {@link ClassPathResource}</li>
* <li>a {@link File} or {@link Path}, using {@link FileSystemResource}</li>
* <li>an {@link InputStream}, using {@link InputStreamResource}</li>
* </ul>
* @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<String, String, JSONCompareResult> 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();
result.passed();
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;
}
}

74
spring-test/src/main/java/org/springframework/test/json/JsonLoader.java

@ -0,0 +1,74 @@ @@ -0,0 +1,74 @@
/*
* 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.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.util.FileCopyUtils;
/**
* Internal helper used to load JSON from various sources.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Stephane Nicoll
* @since 6.2
*/
class JsonLoader {
@Nullable
private final Class<?> resourceLoadClass;
private final Charset charset;
JsonLoader(@Nullable Class<?> resourceLoadClass, @Nullable Charset charset) {
this.resourceLoadClass = resourceLoadClass;
this.charset = (charset != null ? charset : StandardCharsets.UTF_8);
}
@Nullable
String getJson(@Nullable CharSequence source) {
if (source == null) {
return null;
}
if (source.toString().endsWith(".json")) {
return getJson(new ClassPathResource(source.toString(), this.resourceLoadClass));
}
return source.toString();
}
String getJson(Resource source) {
try {
return getJson(source.getInputStream());
}
catch (IOException ex) {
throw new IllegalStateException("Unable to load JSON from " + source, ex);
}
}
private String getJson(InputStream source) throws IOException {
return FileCopyUtils.copyToString(new InputStreamReader(source, this.charset));
}
}

165
spring-test/src/main/java/org/springframework/test/json/JsonPathAssert.java

@ -0,0 +1,165 @@ @@ -0,0 +1,165 @@
/*
* 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<JsonPathAssert, CharSequence> {
private static final Failures failures = Failures.instance();
@Nullable
private final GenericHttpMessageConverter<Object> jsonMessageConverter;
public JsonPathAssert(CharSequence json,
@Nullable GenericHttpMessageConverter<Object> 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<AssertProvider<JsonPathValueAssert>> 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%nTo not match JSON path:%n %s%n", actual, path);
}
}
}
}

48
spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
/*
* 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 com.jayway.jsonpath.JsonPath;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.lang.Nullable;
/**
* AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied
* to a JSON value produced by evaluating a {@linkplain JsonPath JSON path}
* expression.
*
* @author Stephane Nicoll
* @since 6.2
*/
public class JsonPathValueAssert
extends AbstractJsonValueAssert<JsonPathValueAssert> {
private final String expression;
JsonPathValueAssert(@Nullable Object actual, String expression,
@Nullable GenericHttpMessageConverter<Object> httpMessageConverter) {
super(actual, JsonPathValueAssert.class, httpMessageConverter);
this.expression = expression;
}
@Override
protected String getExpectedErrorMessagePrefix() {
return "Expected value at JSON path \"%s\":".formatted(this.expression);
}
}

9
spring-test/src/main/java/org/springframework/test/json/package-info.java

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
/**
* Testing support for JSON.
*/
@NonNullApi
@NonNullFields
package org.springframework.test.json;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

62
spring-test/src/main/java/org/springframework/test/util/MethodAssert.java

@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
/*
* 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.util;
import java.lang.reflect.Method;
import org.assertj.core.api.AbstractObjectAssert;
import org.assertj.core.api.Assertions;
import org.springframework.lang.Nullable;
/**
* AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied
* to a {@link Method}.
*
* @author Stephane Nicoll
* @since 6.2
*/
public class MethodAssert extends AbstractObjectAssert<MethodAssert, Method> {
public MethodAssert(@Nullable Method actual) {
super(actual, MethodAssert.class);
as("Method %s", actual);
}
/**
* Verify that the actual method has the given {@linkplain Method#getName()
* name}.
* @param name the expected method name
*/
public MethodAssert hasName(String name) {
isNotNull();
Assertions.assertThat(this.actual.getName()).as("Method name").isEqualTo(name);
return this.myself;
}
/**
* Verify that the actual method is declared in the given {@code type}.
* @param type the expected declaring class
*/
public MethodAssert hasDeclaringClass(Class<?> type) {
isNotNull();
Assertions.assertThat(this.actual.getDeclaringClass())
.as("Method declaring class").isEqualTo(type);
return this.myself;
}
}

123
spring-test/src/main/java/org/springframework/test/validation/AbstractBindingResultAssert.java

@ -0,0 +1,123 @@ @@ -0,0 +1,123 @@
/*
* 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.validation;
import java.util.List;
import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.AssertProvider;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.ListAssert;
import org.assertj.core.error.BasicErrorMessageFactory;
import org.assertj.core.internal.Failures;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import static org.assertj.core.api.Assertions.assertThat;
/**
* AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to
* {@link BindingResult}.
*
* @author Stephane Nicoll
* @since 6.2
* @param <SELF> the type of assertions
*/
public abstract class AbstractBindingResultAssert<SELF extends AbstractBindingResultAssert<SELF>> extends AbstractAssert<SELF, BindingResult> {
private final Failures failures = Failures.instance();
private final String name;
protected AbstractBindingResultAssert(String name, BindingResult bindingResult, Class<?> selfType) {
super(bindingResult, selfType);
this.name = name;
as("Binding result for attribute '%s", this.name);
}
/**
* Verify that the total number of errors is equal to the given one.
* @param expected the expected number of errors
*/
public SELF hasErrorsCount(int expected) {
assertThat(this.actual.getErrorCount())
.as("check errors for attribute '%s'", this.name).isEqualTo(expected);
return this.myself;
}
/**
* Verify that the actual binding result contains fields in error with the
* given {@code fieldNames}.
* @param fieldNames the names of fields that should be in error
*/
public SELF hasFieldErrors(String... fieldNames) {
assertThat(fieldErrorNames()).contains(fieldNames);
return this.myself;
}
/**
* Verify that the actual binding result contains <em>only</em> fields in
* error with the given {@code fieldNames}, and nothing else.
* @param fieldNames the exhaustive list of field name that should be in error
*/
public SELF hasOnlyFieldErrors(String... fieldNames) {
assertThat(fieldErrorNames()).containsOnly(fieldNames);
return this.myself;
}
/**
* Verify that the field with the given {@code fieldName} has an error
* matching the given {@code errorCode}.
* @param fieldName the name of a field in error
* @param errorCode the error code for that field
*/
public SELF hasFieldErrorCode(String fieldName, String errorCode) {
Assertions.assertThat(getFieldError(fieldName).getCode())
.as("check error code for field '%s'", fieldName).isEqualTo(errorCode);
return this.myself;
}
protected AssertionError unexpectedBindingResult(String reason, Object... arguments) {
return this.failures.failure(this.info, new UnexpectedBindingResult(reason, arguments));
}
private AssertProvider<ListAssert<String>> fieldErrorNames() {
return () -> {
List<String> actual = this.actual.getFieldErrors().stream().map(FieldError::getField).toList();
return new ListAssert<>(actual).as("check field errors");
};
}
private FieldError getFieldError(String fieldName) {
FieldError fieldError = this.actual.getFieldError(fieldName);
if (fieldError == null) {
throw unexpectedBindingResult("to have at least an error for field '%s'", fieldName);
}
return fieldError;
}
private final class UnexpectedBindingResult extends BasicErrorMessageFactory {
private UnexpectedBindingResult(String reason, Object... arguments) {
super("%nExpecting binding result:%n %s%n%s", AbstractBindingResultAssert.this.actual,
reason.formatted(arguments));
}
}
}

9
spring-test/src/main/java/org/springframework/test/validation/package-info.java

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
/**
* Testing support for validation.
*/
@NonNullApi
@NonNullFields
package org.springframework.test.validation;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

101
spring-test/src/main/java/org/springframework/test/web/UriAssert.java

@ -0,0 +1,101 @@ @@ -0,0 +1,101 @@
/*
* 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;
import org.assertj.core.api.AbstractStringAssert;
import org.assertj.core.api.Assertions;
import org.assertj.core.error.BasicErrorMessageFactory;
import org.assertj.core.internal.Failures;
import org.springframework.lang.Nullable;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.util.UriComponentsBuilder;
/**
* AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied
* to a {@link String} representing a URI.
*
* @author Stephane Nicoll
* @since 6.2
*/
public class UriAssert extends AbstractStringAssert<UriAssert> {
private static final AntPathMatcher pathMatcher = new AntPathMatcher();
private final String displayName;
public UriAssert(@Nullable String actual, String displayName) {
super(actual, UriAssert.class);
this.displayName = displayName;
as(displayName);
}
/**
* Verify that the actual URI is equal to the URI built using the given
* {@code uriTemplate} and {@code uriVars}.
* Example: <pre><code class='java'>
* // Verify that uri is equal to "/orders/1/items/2"
* assertThat(uri).isEqualToTemplate("/orders/{orderId}/items/{itemId}", 1, 2));
* </code></pre>
* @param uriTemplate the expected URI string, with a number of URI
* template variables
* @param uriVars the values to replace the URI template variables
* @see UriComponentsBuilder#buildAndExpand(Object...)
*/
public UriAssert isEqualToTemplate(String uriTemplate, Object... uriVars) {
String uri = buildUri(uriTemplate, uriVars);
return isEqualTo(uri);
}
/**
* Verify that the actual URI matches the given {@linkplain AntPathMatcher
* Ant-style} {@code uriPattern}.
* Example: <pre><code class='java'>
* // Verify that pattern matches "/orders/1/items/2"
* assertThat(uri).matchPattern("/orders/*"));
* </code></pre>
* @param uriPattern the pattern that is expected to match
*/
public UriAssert matchPattern(String uriPattern) {
Assertions.assertThat(pathMatcher.isPattern(uriPattern))
.withFailMessage("'%s' is not an Ant-style path pattern", uriPattern).isTrue();
Assertions.assertThat(pathMatcher.match(uriPattern, this.actual))
.withFailMessage("%s '%s' does not match the expected URI pattern '%s'",
this.displayName, this.actual, uriPattern).isTrue();
return this;
}
private String buildUri(String uriTemplate, Object... uriVars) {
try {
return UriComponentsBuilder.fromUriString(uriTemplate)
.buildAndExpand(uriVars).encode().toUriString();
}
catch (Exception ex) {
throw Failures.instance().failure(this.info,
new ShouldBeValidUriTemplate(uriTemplate, ex.getMessage()));
}
}
private static final class ShouldBeValidUriTemplate extends BasicErrorMessageFactory {
private ShouldBeValidUriTemplate(String uriTemplate, String errorMessage) {
super("%nExpecting:%n %s%nTo be a valid URI template but got:%n %s%n", uriTemplate, errorMessage);
}
}
}

122
spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java

@ -0,0 +1,122 @@ @@ -0,0 +1,122 @@
/*
* 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.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.function.Function;
import java.util.function.Supplier;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.assertj.core.api.AbstractObjectAssert;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.MapAssert;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.util.function.SingletonSupplier;
import org.springframework.web.context.request.async.DeferredResult;
/**
* Base AssertJ {@link org.assertj.core.api.Assert assertions} that can be
* applied to a {@link HttpServletRequest}.
*
* @author Stephane Nicoll
* @since 6.2
* @param <SELF> the type of assertions
* @param <ACTUAL> the type of the object to assert
*/
public abstract class AbstractHttpServletRequestAssert<SELF extends AbstractHttpServletRequestAssert<SELF, ACTUAL>, ACTUAL extends HttpServletRequest>
extends AbstractObjectAssert<SELF, ACTUAL> {
private final Supplier<MapAssert<String, Object>> attributesAssertProvider;
private final Supplier<MapAssert<String, Object>> sessionAttributesAssertProvider;
protected AbstractHttpServletRequestAssert(ACTUAL actual, Class<?> selfType) {
super(actual, selfType);
this.attributesAssertProvider = SingletonSupplier.of(() -> createAttributesAssert(actual));
this.sessionAttributesAssertProvider = SingletonSupplier.of(() -> createSessionAttributesAssert(actual));
}
private static MapAssert<String, Object> createAttributesAssert(HttpServletRequest request) {
Map<String, Object> map = toMap(request.getAttributeNames(), request::getAttribute);
return Assertions.assertThat(map).as("Request Attributes");
}
private static MapAssert<String, Object> createSessionAttributesAssert(HttpServletRequest request) {
HttpSession session = request.getSession();
Assertions.assertThat(session).as("HTTP session").isNotNull();
Map<String, Object> map = toMap(session.getAttributeNames(), session::getAttribute);
return Assertions.assertThat(map).as("Session Attributes");
}
/**
* Return a new {@linkplain MapAssert assertion} object that uses the request
* attributes as the object to test, with values mapped by attribute name.
* Examples: <pre><code class='java'>
* // Check for the presence of a request attribute named "attributeName":
* assertThat(request).attributes().containsKey("attributeName");
* </code></pre>
*/
public MapAssert<String, Object> attributes() {
return this.attributesAssertProvider.get();
}
/**
* Return a new {@linkplain MapAssert assertion} object that uses the session
* attributes as the object to test, with values mapped by attribute name.
* Examples: <pre><code class='java'>
* // Check for the presence of a session attribute named "username":
* assertThat(request).sessionAttributes().containsKey("username");
* </code></pre>
*/
public MapAssert<String, Object> sessionAttributes() {
return this.sessionAttributesAssertProvider.get();
}
/**
* Verify that whether asynchronous processing started, usually as a result
* of a controller method returning {@link Callable} or {@link DeferredResult}.
* <p>The test will await the completion of a {@code Callable} so that
* {@link MvcResultAssert#asyncResult()} can be used to assert the resulting
* value.
* <p>Neither a {@code Callable} nor a {@code DeferredResult} will complete
* processing all the way since a {@link MockHttpServletRequest} does not
* perform asynchronous dispatches.
* @param started whether asynchronous processing should have started
*/
public SELF hasAsyncStarted(boolean started) {
Assertions.assertThat(this.actual.isAsyncStarted())
.withFailMessage("Async expected to %s started", (started ? "have" : "not have"))
.isEqualTo(started);
return this.myself;
}
private static Map<String, Object> toMap(Enumeration<String> keys, Function<String, Object> valueProvider) {
Map<String, Object> map = new LinkedHashMap<>();
while (keys.hasMoreElements()) {
String key = keys.nextElement();
map.put(key, valueProvider.apply(key));
}
return map;
}
}

167
spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java

@ -0,0 +1,167 @@ @@ -0,0 +1,167 @@
/*
* 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.util.ArrayList;
import java.util.function.Supplier;
import jakarta.servlet.http.HttpServletResponse;
import org.assertj.core.api.AbstractIntegerAssert;
import org.assertj.core.api.AbstractMapAssert;
import org.assertj.core.api.AbstractObjectAssert;
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.test.http.HttpHeadersAssert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.function.SingletonSupplier;
/**
* Base AssertJ {@link org.assertj.core.api.Assert assertions} that can be
* applied to any object that provides an {@link HttpServletResponse}. This
* allows to provide direct access to response assertions while providing
* access to a different top-level object.
*
* @author Stephane Nicoll
* @since 6.2
* @param <R> the type of {@link HttpServletResponse}
* @param <SELF> the type of assertions
* @param <ACTUAL> the type of the object to assert
*/
public abstract class AbstractHttpServletResponseAssert<R extends HttpServletResponse, SELF extends AbstractHttpServletResponseAssert<R, SELF, ACTUAL>, ACTUAL>
extends AbstractObjectAssert<SELF, ACTUAL> {
private final Supplier<AbstractIntegerAssert<?>> statusAssert;
private final Supplier<HttpHeadersAssert> headersAssertSupplier;
protected AbstractHttpServletResponseAssert(ACTUAL actual, Class<?> selfType) {
super(actual, selfType);
this.statusAssert = SingletonSupplier.of(() -> Assertions.assertThat(getResponse().getStatus()).as("HTTP status code"));
this.headersAssertSupplier = SingletonSupplier.of(() -> new HttpHeadersAssert(getHttpHeaders(getResponse())));
}
/**
* Provide the response to use if it is available. Throw an
* {@link AssertionError} if the request has failed to process and the
* response is not available.
* @return the response to use
*/
protected abstract R getResponse();
/**
* Return a new {@linkplain HttpHeadersAssert assertion} object that uses
* the {@link HttpHeaders} as the object to test. The return assertion
* object provides all the regular {@linkplain AbstractMapAssert map
* assertions}, with headers mapped by header name.
* Examples: <pre><code class='java'>
* // Check for the presence of the Accept header:
* assertThat(response).headers().containsHeader(HttpHeaders.ACCEPT);
* // Check for the absence of the Content-Length header:
* assertThat(response).headers().doesNotContainsHeader(HttpHeaders.CONTENT_LENGTH);
* </code></pre>
*/
public HttpHeadersAssert headers() {
return this.headersAssertSupplier.get();
}
/**
* Verify that the HTTP status is equal to the specified status code.
* @param status the expected HTTP status code
*/
public SELF hasStatus(int status) {
status().isEqualTo(status);
return this.myself;
}
/**
* Verify that the HTTP status is equal to the specified
* {@linkplain HttpStatus status}.
* @param status the expected HTTP status code
*/
public SELF hasStatus(HttpStatus status) {
return hasStatus(status.value());
}
/**
* Verify that the HTTP status is equal to {@link HttpStatus#OK}.
* @see #hasStatus(HttpStatus)
*/
public SELF hasStatusOk() {
return hasStatus(HttpStatus.OK);
}
/**
* Verify that the HTTP status code is in the 1xx range.
* @see <a href="https://datatracker.ietf.org/doc/html/rfc2616#section-10.1">RFC 2616</a>
*/
public SELF hasStatus1xxInformational() {
return hasStatusSeries(Series.INFORMATIONAL);
}
/**
* Verify that the HTTP status code is in the 2xx range.
* @see <a href="https://datatracker.ietf.org/doc/html/rfc2616#section-10.2">RFC 2616</a>
*/
public SELF hasStatus2xxSuccessful() {
return hasStatusSeries(Series.SUCCESSFUL);
}
/**
* Verify that the HTTP status code is in the 3xx range.
* @see <a href="https://datatracker.ietf.org/doc/html/rfc2616#section-10.3">RFC 2616</a>
*/
public SELF hasStatus3xxRedirection() {
return hasStatusSeries(Series.REDIRECTION);
}
/**
* Verify that the HTTP status code is in the 4xx range.
* @see <a href="https://datatracker.ietf.org/doc/html/rfc2616#section-10.4">RFC 2616</a>
*/
public SELF hasStatus4xxClientError() {
return hasStatusSeries(Series.CLIENT_ERROR);
}
/**
* Verify that the HTTP status code is in the 5xx range.
* @see <a href="https://datatracker.ietf.org/doc/html/rfc2616#section-10.5">RFC 2616</a>
*/
public SELF hasStatus5xxServerError() {
return hasStatusSeries(Series.SERVER_ERROR);
}
private SELF hasStatusSeries(Series series) {
Assertions.assertThat(Series.resolve(getResponse().getStatus())).as("HTTP status series").isEqualTo(series);
return this.myself;
}
private AbstractIntegerAssert<?> status() {
return this.statusAssert.get();
}
private static HttpHeaders getHttpHeaders(HttpServletResponse response) {
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
response.getHeaderNames().forEach(name -> headers.put(name, new ArrayList<>(response.getHeaders(name))));
return new HttpHeaders(headers);
}
}

38
spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssert.java

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
/*
* 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 org.springframework.mock.web.MockHttpServletRequest;
/**
* AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to
* {@link MockHttpServletRequest}.
*
* @author Stephane Nicoll
* @since 6.2
* @param <SELF> the type of assertions
*/
public abstract class AbstractMockHttpServletRequestAssert<SELF extends AbstractMockHttpServletRequestAssert<SELF>>
extends AbstractHttpServletRequestAssert<SELF, MockHttpServletRequest> {
protected AbstractMockHttpServletRequestAssert(MockHttpServletRequest request, Class<?> selfType) {
super(request, selfType);
}
}

109
spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java

@ -0,0 +1,109 @@ @@ -0,0 +1,109 @@
/*
* 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 org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.UriAssert;
/**
* Extension of {@link AbstractHttpServletResponseAssert} for
* {@link MockHttpServletResponse}.
*
* @author Stephane Nicoll
* @since 6.2
* @param <SELF> the type of assertions
* @param <ACTUAL> the type of the object to assert
*/
public abstract class AbstractMockHttpServletResponseAssert<SELF extends AbstractMockHttpServletResponseAssert<SELF, ACTUAL>, ACTUAL>
extends AbstractHttpServletResponseAssert<MockHttpServletResponse, SELF, ACTUAL> {
@Nullable
private final GenericHttpMessageConverter<Object> jsonMessageConverter;
protected AbstractMockHttpServletResponseAssert(
@Nullable GenericHttpMessageConverter<Object> jsonMessageConverter, ACTUAL actual, Class<?> selfType) {
super(actual, selfType);
this.jsonMessageConverter = jsonMessageConverter;
}
/**
* Return a new {@linkplain ResponseBodyAssert assertion} object that uses
* the response body as the object to test. The return assertion object
* provides access to the raw byte array, a String value decoded using the
* response's character encoding, and dedicated json testing support.
* Examples: <pre><code class='java'>
* // 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");
* </code></pre>
*/
public ResponseBodyAssert body() {
return new ResponseBodyAssert(getResponse().getContentAsByteArray(),
Charset.forName(getResponse().getCharacterEncoding()), this.jsonMessageConverter);
}
/**
* Return a new {@linkplain UriAssert assertion} object that uses the
* forwarded URL as the object to test. If a simple equality check is
* required consider using {@link #hasForwardedUrl(String)} instead.
* Example: <pre><code class='java'>
* // Check that the forwarded URL starts with "/orders/":
* assertThat(response).forwardedUrl().matchPattern("/orders/*);
* </code></pre>
*/
public UriAssert forwardedUrl() {
return new UriAssert(getResponse().getForwardedUrl(), "Forwarded URL");
}
/**
* Return a new {@linkplain UriAssert assertion} object that uses the
* redirected URL as the object to test. If a simple equality check is
* required consider using {@link #hasRedirectedUrl(String)} instead.
* Example: <pre><code class='java'>
* // Check that the redirected URL starts with "/orders/":
* assertThat(response).redirectedUrl().matchPattern("/orders/*);
* </code></pre>
*/
public UriAssert redirectedUrl() {
return new UriAssert(getResponse().getRedirectedUrl(), "Redirected URL");
}
/**
* Verify that the forwarded URL is equal to the given value.
* @param forwardedUrl the expected forwarded URL (can be null)
*/
public SELF hasForwardedUrl(@Nullable String forwardedUrl) {
forwardedUrl().isEqualTo(forwardedUrl);
return this.myself;
}
/**
* Verify that the redirected URL is equal to the given value.
* @param redirectedUrl the expected redirected URL (can be null)
*/
public SELF hasRedirectedUrl(@Nullable String redirectedUrl) {
redirectedUrl().isEqualTo(redirectedUrl);
return this.myself;
}
}

227
spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMockMvc.java

@ -0,0 +1,227 @@ @@ -0,0 +1,227 @@
/*
* 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.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.StreamSupport;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder;
import org.springframework.util.Assert;
import org.springframework.web.context.WebApplicationContext;
/**
* {@link MockMvc} variant that tests Spring MVC exchanges and provide fluent
* assertions using {@link org.assertj.core.api.Assertions AssertJ}.
*
* <p>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}.
*
* <p>{@link AssertableMockMvc} can be configured with a list of
* {@linkplain HttpMessageConverter HttpMessageConverters} to allow response
* body to be deserialized, rather than asserting on the raw values.
*
* @author Stephane Nicoll
* @author Brian Clozel
* @since 6.2
*/
public final class AssertableMockMvc {
private static final MediaType JSON = MediaType.APPLICATION_JSON;
private final MockMvc mockMvc;
@Nullable
private final GenericHttpMessageConverter<Object> jsonMessageConverter;
private AssertableMockMvc(MockMvc mockMvc, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) {
Assert.notNull(mockMvc, "mockMVC should not be null");
this.mockMvc = mockMvc;
this.jsonMessageConverter = jsonMessageConverter;
}
/**
* Create a new {@link AssertableMockMvc} instance that delegates to the
* given {@link MockMvc}.
* @param mockMvc the MockMvc instance to delegate calls to
*/
public static AssertableMockMvc create(MockMvc mockMvc) {
return new AssertableMockMvc(mockMvc, null);
}
/**
* Create a {@link AssertableMockMvc} instance using the given, fully
* initialized (i.e., <em>refreshed</em>) {@link WebApplicationContext}. The
* given {@code customizations} are applied to the {@link DefaultMockMvcBuilder}
* that ultimately creates the underlying {@link MockMvc} instance.
* <p>If no further customization of the underlying {@link MockMvc} instance
* is required, use {@link #from(WebApplicationContext)}.
* @param applicationContext the application context to detect the Spring
* MVC infrastructure and application controllers from
* @param customizations the function that creates a {@link MockMvc}
* instance based on a {@link DefaultMockMvcBuilder}.
* @see MockMvcBuilders#webAppContextSetup(WebApplicationContext)
*/
public static AssertableMockMvc from(WebApplicationContext applicationContext,
Function<DefaultMockMvcBuilder, MockMvc> customizations) {
DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(applicationContext);
MockMvc mockMvc = customizations.apply(builder);
return create(mockMvc);
}
/**
* Shortcut to create a {@link AssertableMockMvc} instance using the given,
* fully initialized (i.e., <em>refreshed</em>) {@link WebApplicationContext}.
* <p>Consider using {@link #from(WebApplicationContext, Function)} if
* further customizations of the underlying {@link MockMvc} instance is
* required.
* @param applicationContext the application context to detect the Spring
* MVC infrastructure and application controllers from
* @see MockMvcBuilders#webAppContextSetup(WebApplicationContext)
*/
public static AssertableMockMvc from(WebApplicationContext applicationContext) {
return from(applicationContext, DefaultMockMvcBuilder::build);
}
/**
* Create a {@link AssertableMockMvc} instance by registering one or more
* {@code @Controller} instances and configuring Spring MVC infrastructure
* programmatically.
* <p>This allows full control over the instantiation and initialization of
* controllers and their dependencies, similar to plain unit tests while
* also making it possible to test one controller at a time.
* @param controllers one or more {@code @Controller} instances to test
* (specified {@code Class} will be turned into instance)
* @param customizations the function that creates a {@link MockMvc}
* instance based on a {@link StandaloneMockMvcBuilder}, typically to
* configure the Spring MVC infrastructure
* @see MockMvcBuilders#standaloneSetup(Object...)
*/
public static AssertableMockMvc of(Collection<?> controllers,
Function<StandaloneMockMvcBuilder, MockMvc> customizations) {
StandaloneMockMvcBuilder builder = MockMvcBuilders.standaloneSetup(controllers.toArray());
return create(customizations.apply(builder));
}
/**
* Shortcut to create a {@link AssertableMockMvc} instance by registering
* one or more {@code @Controller} instances.
* <p>The minimum infrastructure required by the
* {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlet}
* to serve requests with annotated controllers is created. Consider using
* {@link #of(Collection, Function)} if additional configuration of the MVC
* infrastructure is required.
* @param controllers one or more {@code @Controller} instances to test
* (specified {@code Class} will be turned into instance)
* @see MockMvcBuilders#standaloneSetup(Object...)
*/
public static AssertableMockMvc of(Object... controllers) {
return of(Arrays.asList(controllers), StandaloneMockMvcBuilder::build);
}
/**
* Return a new {@link AssertableMockMvc} instance using the specified
* {@link HttpMessageConverter}. If none are specified, only basic assertions
* on the response body can be performed. Consider registering a suitable
* JSON converter for asserting data structure.
* @param httpMessageConverters the message converters to use
* @return a new instance using the specified converters
*/
public AssertableMockMvc withHttpMessageConverters(Iterable<HttpMessageConverter<?>> httpMessageConverters) {
return new AssertableMockMvc(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.
* <p>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
* against "/greet" has an HTTP status code 200 (OK), and a simple body:
* <pre><code class='java'>assertThat(mvc.perform(get("/greet")))
* .hasStatusOk()
* .body().asString().isEqualTo("Hello");
* </code></pre>
* <p>Contrary to {@link MockMvc#perform(RequestBuilder)}, this does not
* throw an exception if the request fails with an unresolved exception.
* Rather, the result provides the exception, if any. Assuming that a
* {@linkplain MockMvcRequestBuilders#post(URI) POST} request against
* {@code /boom} throws an {@code IllegalStateException}, the following
* asserts that the invocation has indeed failed with the expected error
* message:
* <pre><code class='java'>assertThat(mvc.perform(post("/boom")))
* .unresolvedException().isInstanceOf(IllegalStateException.class)
* .hasMessage("Expected");
* </code></pre>
* <p>
* @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}
* @see MockMvc#perform(RequestBuilder)
*/
public AssertableMvcResult perform(RequestBuilder requestBuilder) {
Object result = getMvcResultOrFailure(requestBuilder);
if (result instanceof MvcResult mvcResult) {
return new DefaultAssertableMvcResult(mvcResult, null, this.jsonMessageConverter);
}
else {
return new DefaultAssertableMvcResult(null, (Exception) result, this.jsonMessageConverter);
}
}
private Object getMvcResultOrFailure(RequestBuilder requestBuilder) {
try {
return this.mockMvc.perform(requestBuilder).andReturn();
}
catch (Exception ex) {
return ex;
}
}
@SuppressWarnings("unchecked")
@Nullable
private GenericHttpMessageConverter<Object> findJsonMessageConverter(
Iterable<HttpMessageConverter<?>> messageConverters) {
return StreamSupport.stream(messageConverters.spliterator(), false)
.filter(GenericHttpMessageConverter.class::isInstance)
.map(GenericHttpMessageConverter.class::cast)
.filter(converter -> converter.canWrite(null, Map.class, JSON))
.filter(converter -> converter.canRead(Map.class, JSON))
.findFirst().orElse(null);
}
}

50
spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AssertableMvcResult.java

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
/*
* 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 org.assertj.core.api.AssertProvider;
import org.springframework.lang.Nullable;
import org.springframework.test.web.servlet.MvcResult;
/**
* A {@link MvcResult} that additionally supports AssertJ style assertions.
*
* <p>Can be in two distinct states:
* <ol>
* <li>The request processed successfully, and {@link #getUnresolvedException()}
* is therefore {@code null}.</li>
* <li>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.</li>
* </ol>
*
* @author Stephane Nicoll
* @author Brian Clozel
* @since 6.2
* @see AssertableMockMvc
*/
public interface AssertableMvcResult extends MvcResult, AssertProvider<MvcResultAssert> {
/**
* Return the exception that was thrown unexpectedly while processing the
* request, if any.
*/
@Nullable
Exception getUnresolvedException();
}

173
spring-test/src/main/java/org/springframework/test/web/servlet/assertj/CookieMapAssert.java

@ -0,0 +1,173 @@ @@ -0,0 +1,173 @@
/*
* 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.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
import jakarta.servlet.http.Cookie;
import org.assertj.core.api.AbstractMapAssert;
import org.assertj.core.api.Assertions;
/**
* AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to
* {@link Cookie cookies}.
*
* @author Brian Clozel
* @author Stephane Nicoll
* @since 6.2
*/
public class CookieMapAssert extends AbstractMapAssert<CookieMapAssert, Map<String, Cookie>, String, Cookie> {
public CookieMapAssert(Cookie[] actual) {
super(mapCookies(actual), CookieMapAssert.class);
as("Cookies");
}
private static Map<String, Cookie> mapCookies(Cookie[] cookies) {
Map<String, Cookie> map = new LinkedHashMap<>();
for (Cookie cookie : cookies) {
map.putIfAbsent(cookie.getName(), cookie);
}
return map;
}
/**
* Verify that the actual cookies contain a cookie with the given {@code name}.
* @param name the name of an expected cookie
* @see #containsKey
*/
public CookieMapAssert containsCookie(String name) {
return containsKey(name);
}
/**
* Verify that the actual cookies contain the cookies with the given
* {@code names}.
* @param names the names of expected cookies
* @see #containsKeys
*/
public CookieMapAssert containsCookies(String... names) {
return containsKeys(names);
}
/**
* Verify that the actual cookies do not contain a cookie with the
* given {@code name}.
* @param name the name of a cookie that should not be present
* @see #doesNotContainKey
*/
public CookieMapAssert doesNotContainCookie(String name) {
return doesNotContainKey(name);
}
/**
* Verify that the actual cookies do not contain any of the cookies with
* the given {@code names}.
* @param names the names of cookies that should not be present
* @see #doesNotContainKeys
*/
public CookieMapAssert doesNotContainCookies(String... names) {
return doesNotContainKeys(names);
}
/**
* Verify that the actual cookies contain a cookie with the given
* {@code name} that satisfy given {@code cookieRequirements}.
* the specified names.
* @param name the name of an expected cookie
* @param cookieRequirements the requirements for the cookie
*/
public CookieMapAssert hasCookieSatisfying(String name, Consumer<Cookie> cookieRequirements) {
return hasEntrySatisfying(name, cookieRequirements);
}
/**
* Verify that the actual cookies contain a cookie with the given
* {@code name} whose {@linkplain Cookie#getValue() value} is equal to the
* given one.
* @param name the name of the cookie
* @param expected the expected value of the cookie
*/
public CookieMapAssert hasValue(String name, String expected) {
return hasCookieSatisfying(name, cookie ->
Assertions.assertThat(cookie.getValue()).isEqualTo(expected));
}
/**
* Verify that the actual cookies contain a cookie with the given
* {@code name} whose {@linkplain Cookie#getMaxAge() max age} is equal to
* the given one.
* @param name the name of the cookie
* @param expected the expected max age of the cookie
*/
public CookieMapAssert hasMaxAge(String name, Duration expected) {
return hasCookieSatisfying(name, cookie ->
Assertions.assertThat(Duration.ofSeconds(cookie.getMaxAge())).isEqualTo(expected));
}
/**
* Verify that the actual cookies contain a cookie with the given
* {@code name} whose {@linkplain Cookie#getPath() path} is equal to
* the given one.
* @param name the name of the cookie
* @param expected the expected path of the cookie
*/
public CookieMapAssert hasPath(String name, String expected) {
return hasCookieSatisfying(name, cookie ->
Assertions.assertThat(cookie.getPath()).isEqualTo(expected));
}
/**
* Verify that the actual cookies contain a cookie with the given
* {@code name} whose {@linkplain Cookie#getDomain() domain} is equal to
* the given one.
* @param name the name of the cookie
* @param expected the expected path of the cookie
*/
public CookieMapAssert hasDomain(String name, String expected) {
return hasCookieSatisfying(name, cookie ->
Assertions.assertThat(cookie.getDomain()).isEqualTo(expected));
}
/**
* Verify that the actual cookies contain a cookie with the given
* {@code name} whose {@linkplain Cookie#getSecure() secure flag} is equal
* to the given one.
* @param name the name of the cookie
* @param expected whether the cookie is secure
*/
public CookieMapAssert isSecure(String name, boolean expected) {
return hasCookieSatisfying(name, cookie ->
Assertions.assertThat(cookie.getSecure()).isEqualTo(expected));
}
/**
* Verify that the actual cookies contain a cookie with the given
* {@code name} whose {@linkplain Cookie#isHttpOnly() http only flag} is
* equal to the given one.
* @param name the name of the cookie
* @param expected whether the cookie is http only
*/
public CookieMapAssert isHttpOnly(String name, boolean expected) {
return hasCookieSatisfying(name, cookie ->
Assertions.assertThat(cookie.isHttpOnly()).isEqualTo(expected));
}
}

123
spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResult.java

@ -0,0 +1,123 @@ @@ -0,0 +1,123 @@
/*
* 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 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.web.servlet.MvcResult;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
/**
* The default {@link AssertableMvcResult} implementation.
*
* @author Stephane Nicoll
* @since 6.2
*/
final class DefaultAssertableMvcResult implements AssertableMvcResult {
@Nullable
private final MvcResult target;
@Nullable
private final Exception unresolvedException;
@Nullable
private final GenericHttpMessageConverter<Object> jsonMessageConverter;
DefaultAssertableMvcResult(@Nullable MvcResult target, @Nullable Exception unresolvedException, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) {
this.target = target;
this.unresolvedException = unresolvedException;
this.jsonMessageConverter = jsonMessageConverter;
}
/**
* Return the exception that was thrown unexpectedly while processing the
* request, if any.
*/
@Nullable
public Exception getUnresolvedException() {
return this.unresolvedException;
}
@Override
public MockHttpServletRequest getRequest() {
return getTarget().getRequest();
}
@Override
public MockHttpServletResponse getResponse() {
return getTarget().getResponse();
}
@Override
public Object getHandler() {
return getTarget().getHandler();
}
@Override
public HandlerInterceptor[] getInterceptors() {
return getTarget().getInterceptors();
}
@Override
public ModelAndView getModelAndView() {
return getTarget().getModelAndView();
}
@Override
public Exception getResolvedException() {
return getTarget().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);
}
}

120
spring-test/src/main/java/org/springframework/test/web/servlet/assertj/HandlerResultAssert.java

@ -0,0 +1,120 @@ @@ -0,0 +1,120 @@
/*
* 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.lang.reflect.Method;
import org.assertj.core.api.AbstractObjectAssert;
import org.assertj.core.api.Assertions;
import org.springframework.cglib.core.internal.Function;
import org.springframework.lang.Nullable;
import org.springframework.test.util.MethodAssert;
import org.springframework.util.ClassUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.MethodInvocationInfo;
/**
* AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to
* a handler or handler method.
* @author Stephane Nicoll
* @since 6.2
*/
public class HandlerResultAssert extends AbstractObjectAssert<HandlerResultAssert, Object> {
public HandlerResultAssert(@Nullable Object actual) {
super(actual, HandlerResultAssert.class);
as("Handler result");
}
/**
* Return a new {@linkplain MethodAssert assertion} object that uses
* the {@link Method} that handles the request as the object to test.
* Verify first that the handler is a {@linkplain #isMethodHandler() method
* handler}.
* Example: <pre><code class='java'>
* // Check that a GET to "/greet" is invoked on a "handleGreet" method name
* assertThat(mvc.perform(get("/greet")).handler().method().hasName("sayGreet");
* </code></pre>
*/
public MethodAssert method() {
return new MethodAssert(getHandlerMethod());
}
/**
* Verify that the handler is managed by a method invocation, typically on
* a controller.
*/
public HandlerResultAssert isMethodHandler() {
return isNotNull().isInstanceOf(HandlerMethod.class);
}
/**
* Verify that the handler is managed by the given {@code handlerMethod}.
* This creates a "mock" for the given {@code controllerType} and record the
* method invocation in the {@code handlerMethod}. The arguments used by the
* target method invocation can be {@code null} as the purpose of the mock
* is to identify the method that was invoked.
* Example: <pre><code class='java'>
* // If the method has a return type, you can return the result of the invocation
* assertThat(mvc.perform(get("/greet")).handler().isInvokedOn(
* GreetController.class, controller -> controller.sayGreet());
* // If the method has a void return type, the controller should be returned
* assertThat(mvc.perform(post("/persons/")).handler().isInvokedOn(
* PersonController.class, controller -> controller.createPerson(null, null));
* </code></pre>
* @param controllerType the controller to mock
* @param handlerMethod the method
*/
public <T> HandlerResultAssert isInvokedOn(Class<T> controllerType, Function<T, Object> handlerMethod) {
MethodAssert actual = method();
Object methodInvocationInfo = handlerMethod.apply(MvcUriComponentsBuilder.on(controllerType));
Assertions.assertThat(methodInvocationInfo)
.as("Method invocation on controller '%s'", controllerType.getSimpleName())
.isInstanceOfSatisfying(MethodInvocationInfo.class, mii ->
actual.isEqualTo(mii.getControllerMethod()));
return this;
}
/**
* Verify that the handler is of the given {@code type}. For a controller
* method, this is the type of the controller.
* Example: <pre><code class='java'>
* // Check that a GET to "/greet" is managed by GreetController
* assertThat(mvc.perform(get("/greet")).handler().hasType(GreetController.class);
* </code></pre>
* @param type the expected type of the handler
*/
public HandlerResultAssert hasType(Class<?> type) {
isNotNull();
Class<?> actualType = this.actual.getClass();
if (this.actual instanceof HandlerMethod handlerMethod) {
actualType = handlerMethod.getBeanType();
}
Assertions.assertThat(ClassUtils.getUserClass(actualType)).as("Handler result type").isEqualTo(type);
return this;
}
private Method getHandlerMethod() {
isMethodHandler(); // validate type
return ((HandlerMethod) this.actual).getMethod();
}
}

163
spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java

@ -0,0 +1,163 @@ @@ -0,0 +1,163 @@
/*
* 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.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import org.assertj.core.api.AbstractMapAssert;
import org.assertj.core.error.BasicErrorMessageFactory;
import org.assertj.core.internal.Failures;
import org.springframework.lang.Nullable;
import org.springframework.test.validation.AbstractBindingResultAssert;
import org.springframework.validation.BindingResult;
import org.springframework.validation.BindingResultUtils;
import org.springframework.validation.Errors;
import org.springframework.web.servlet.ModelAndView;
/**
* AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to
* a {@linkplain ModelAndView#getModel() model}.
*
* @author Stephane Nicoll
* @since 6.2
*/
public class ModelAssert extends AbstractMapAssert<ModelAssert, Map<String, Object>, String, Object> {
private final Failures failures = Failures.instance();
public ModelAssert(Map<String, Object> map) {
super(map, ModelAssert.class);
}
/**
* Return a new {@linkplain AbstractBindingResultAssert assertion} object
* that uses the {@link BindingResult} with the given {@code name} as the
* object to test.
* Examples: <pre><code class='java'>
* // Check that the "person" attribute in the model has 2 errors:
* assertThat(...).model().extractingBindingResult("person").hasErrorsCount(2);
* </code></pre>
*/
public AbstractBindingResultAssert<?> extractingBindingResult(String name) {
BindingResult result = BindingResultUtils.getBindingResult(this.actual, name);
if (result == null) {
throw unexpectedModel("to have a binding result for attribute '%s'", name);
}
return new BindingResultAssert(name, result);
}
/**
* Verify that the actual model has at least one error.
*/
public ModelAssert hasErrors() {
if (getAllErrors() == 0) {
throw unexpectedModel("to have at least one error");
}
return this.myself;
}
/**
* Verify that the actual model does not have any errors.
*/
public ModelAssert doesNotHaveErrors() {
int count = getAllErrors(); if (count > 0) {
throw unexpectedModel("to not have an error, but got %s", count);
}
return this.myself;
}
/**
* Verify that the actual model contain the attributes with the given
* {@code names}, and that these attributes have each at least one error.
* @param names the expected names of attributes with errors
*/
public ModelAssert hasAttributeErrors(String... names) {
return assertAttributes(names, BindingResult::hasErrors,
"to have attribute errors for", "these attributes do not have any error");
}
/**
* Verify that the actual model contain the attributes with the given
* {@code names}, and that these attributes do not have any error.
* @param names the expected names of attributes without errors
*/
public ModelAssert doesNotHaveAttributeErrors(String... names) {
return assertAttributes(names, Predicate.not(BindingResult::hasErrors),
"to have attribute without errors for", "these attributes have at least an error");
}
private ModelAssert assertAttributes(String[] names, Predicate<BindingResult> condition,
String assertionMessage, String failAssertionMessage) {
Set<String> missing = new LinkedHashSet<>();
Set<String> failCondition = new LinkedHashSet<>();
for (String name : names) {
BindingResult bindingResult = getBindingResult(name);
if (bindingResult == null) {
missing.add(name);
}
else if (!condition.test(bindingResult)) {
failCondition.add(name);
}
}
if (!missing.isEmpty() || !failCondition.isEmpty()) {
StringBuilder sb = new StringBuilder();
sb.append("%n%s:%n %s%n".formatted(assertionMessage, String.join(", ", names)));
if (!missing.isEmpty()) {
sb.append("%nbut could not find these attributes:%n %s%n".formatted(String.join(", ", missing)));
}
if (!failCondition.isEmpty()) {
String prefix = missing.isEmpty() ? "but" : "and";
sb.append("%n%s %s:%n %s%n".formatted(prefix, failAssertionMessage, String.join(", ", failCondition)));
}
throw unexpectedModel(sb.toString());
}
return this.myself;
}
private AssertionError unexpectedModel(String reason, Object... arguments) {
return this.failures.failure(this.info, new UnexpectedModel(reason, arguments));
}
private int getAllErrors() {
return this.actual.values().stream().filter(Errors.class::isInstance).map(Errors.class::cast)
.map(Errors::getErrorCount).reduce(0, Integer::sum);
}
@Nullable
private BindingResult getBindingResult(String name) {
return BindingResultUtils.getBindingResult(this.actual, name);
}
private final class UnexpectedModel extends BasicErrorMessageFactory {
private UnexpectedModel(String reason, Object... arguments) {
super("%nExpecting model:%n %s%n%s", ModelAssert.this.actual, reason.formatted(arguments));
}
}
private static final class BindingResultAssert extends AbstractBindingResultAssert<BindingResultAssert> {
public BindingResultAssert(String name, BindingResult bindingResult) {
super(name, bindingResult, BindingResultAssert.class);
}
}
}

258
spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcResultAssert.java

@ -0,0 +1,258 @@ @@ -0,0 +1,258 @@
/*
* 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.BufferedReader;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import jakarta.servlet.http.Cookie;
import org.assertj.core.api.AbstractStringAssert;
import org.assertj.core.api.AbstractThrowableAssert;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.MapAssert;
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;
import org.springframework.web.servlet.ModelAndView;
/**
* AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied
* to {@link MvcResult}.
*
* @author Stephane Nicoll
* @author Brian Clozel
* @since 6.2
*/
public class MvcResultAssert extends AbstractMockHttpServletResponseAssert<MvcResultAssert, AssertableMvcResult> {
MvcResultAssert(AssertableMvcResult mvcResult, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) {
super(jsonMessageConverter, mvcResult, MvcResultAssert.class);
}
@Override
protected MockHttpServletResponse getResponse() {
checkHasNotFailedUnexpectedly();
return this.actual.getResponse();
}
/**
* Verify that the request has failed with an unresolved exception, and
* return a new {@linkplain AbstractThrowableAssert assertion} object
* that uses the unresolved {@link Exception} as the object to test.
*/
public AbstractThrowableAssert<?, ? extends Throwable> unresolvedException() {
hasUnresolvedException();
return Assertions.assertThat(this.actual.getUnresolvedException());
}
/**
* Return a new {@linkplain AbstractMockHttpServletRequestAssert assertion}
* object that uses the {@link MockHttpServletRequest} as the object to test.
*/
public AbstractMockHttpServletRequestAssert<?> request() {
checkHasNotFailedUnexpectedly();
return new MockHttpRequestAssert(this.actual.getRequest());
}
/**
* Return a new {@linkplain CookieMapAssert assertion} object that uses the
* response's {@linkplain Cookie cookies} as the object to test.
*/
public CookieMapAssert cookies() {
checkHasNotFailedUnexpectedly();
return new CookieMapAssert(this.actual.getResponse().getCookies());
}
/**
* Return a new {@linkplain MediaTypeAssert assertion} object that uses the
* response's {@linkplain MediaType content type} as the object to test.
*/
public MediaTypeAssert contentType() {
checkHasNotFailedUnexpectedly();
return new MediaTypeAssert(this.actual.getResponse().getContentType());
}
/**
* Return a new {@linkplain HandlerResultAssert assertion} object that uses
* the handler as the object to test. For a method invocation on a
* controller, this is relative method handler
* Example: <pre><code class='java'>
* // Check that a GET to "/greet" is invoked on a "handleGreet" method name
* assertThat(mvc.perform(get("/greet")).handler().method().hasName("sayGreet");
* </code></pre>
*/
public HandlerResultAssert handler() {
checkHasNotFailedUnexpectedly();
return new HandlerResultAssert(this.actual.getHandler());
}
/**
* Verify that a {@link ModelAndView} is available and return a new
* {@linkplain ModelAssert assertion} object that uses the
* {@linkplain ModelAndView#getModel() model} as the object to test.
*/
public ModelAssert model() {
checkHasNotFailedUnexpectedly();
return new ModelAssert(getModelAndView().getModel());
}
/**
* Verify that a {@link ModelAndView} is available and return a new
* {@linkplain AbstractStringAssert assertion} object that uses the
* {@linkplain ModelAndView#getViewName()} view name} as the object to test.
* @see #hasViewName(String)
*/
public AbstractStringAssert<?> viewName() {
checkHasNotFailedUnexpectedly();
return Assertions.assertThat(getModelAndView().getViewName()).as("View name");
}
/**
* Return a new {@linkplain MapAssert assertion} object that uses the
* "output" flash attributes saved during request processing as the object
* to test.
*/
public MapAssert<String, Object> flash() {
checkHasNotFailedUnexpectedly();
return new MapAssert<>(this.actual.getFlashMap());
}
/**
* Verify that an {@linkplain AbstractHttpServletRequestAssert#hasAsyncStarted(boolean)
* asynchronous processing has started} and return a new
* {@linkplain ObjectAssert assertion} object that uses the asynchronous
* result as the object to test.
*/
public ObjectAssert<Object> asyncResult() {
request().hasAsyncStarted(true);
return Assertions.assertThat(this.actual.getAsyncResult()).as("Async result");
}
/**
* Verify that the request has failed with an unresolved exception.
* @see #unresolvedException()
*/
public MvcResultAssert hasUnresolvedException() {
Assertions.assertThat(this.actual.getUnresolvedException())
.withFailMessage("Expecting request to have failed but it has succeeded").isNotNull();
return this;
}
/**
* Verify that the request has not failed with an unresolved exception.
*/
public MvcResultAssert doesNotHaveUnresolvedException() {
Assertions.assertThat(this.actual.getUnresolvedException())
.withFailMessage("Expecting request to have succeeded but it has failed").isNull();
return this;
}
/**
* Verify that the actual mvc result matches the given {@link ResultMatcher}.
* @param resultMatcher the result matcher to invoke
*/
public MvcResultAssert matches(ResultMatcher resultMatcher) {
checkHasNotFailedUnexpectedly();
return super.satisfies(resultMatcher::match);
}
/**
* 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);
}
/**
* Verify that a {@link ModelAndView} is available with a view equals to
* the given one. For more advanced assertions, consider using
* {@link #viewName()}
* @param viewName the expected view name
*/
public MvcResultAssert hasViewName(String viewName) {
viewName().isEqualTo(viewName);
return this.myself;
}
private ModelAndView getModelAndView() {
ModelAndView modelAndView = this.actual.getModelAndView();
Assertions.assertThat(modelAndView).as("ModelAndView").isNotNull();
return modelAndView;
}
protected void checkHasNotFailedUnexpectedly() {
Exception unresolvedException = this.actual.getUnresolvedException();
if (unresolvedException != null) {
throw Failures.instance().failure(this.info,
new RequestFailedUnexpectedly(unresolvedException));
}
}
private static final class MockHttpRequestAssert extends AbstractMockHttpServletRequestAssert<MockHttpRequestAssert> {
private MockHttpRequestAssert(MockHttpServletRequest request) {
super(request, MockHttpRequestAssert.class);
}
}
private static final class RequestFailedUnexpectedly extends BasicErrorMessageFactory {
private RequestFailedUnexpectedly(Exception ex) {
super("%nRequest has failed unexpectedly:%n%s", unquotedString(getIndentedStackTraceAsString(ex)));
}
private static String getIndentedStackTraceAsString(Throwable ex) {
String stackTrace = getStackTraceAsString(ex);
return indent(stackTrace);
}
private static String getStackTraceAsString(Throwable ex) {
StringWriter writer = new StringWriter();
PrintWriter printer = new PrintWriter(writer);
ex.printStackTrace(printer);
return writer.toString();
}
private static String indent(String input) {
BufferedReader reader = new BufferedReader(new StringReader(input));
StringWriter writer = new StringWriter();
PrintWriter printer = new PrintWriter(writer);
reader.lines().forEach(line -> {
printer.print(" ");
printer.println(line);
});
return writer.toString();
}
}
}

125
spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java

@ -0,0 +1,125 @@ @@ -0,0 +1,125 @@
/*
* 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<ResponseBodyAssert> {
private final Charset characterEncoding;
@Nullable
private final GenericHttpMessageConverter<Object> jsonMessageConverter;
ResponseBodyAssert(byte[] actual, Charset characterEncoding,
@Nullable GenericHttpMessageConverter<Object> 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 {@linkplain org.skyscreamer.jsonassert.JSONCompareMode JSON
* assert} comparison to expected json input that can be loaded from the
* classpath. Only absolute locations are supported, consider using
* {@link #json(Class)} to load json documents relative to a given class.
* Example: <pre><code class='java'>
* // 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");
* </code></pre>
*/
public JsonContentAssert json() {
return json(null);
}
/**
* Return a new {@linkplain JsonContentAssert assertion} object that
* provides {@linkplain org.skyscreamer.jsonassert.JSONCompareMode JSON
* assert} comparison to expected json input that can be loaded from the
* classpath. Documents can be absolute using a leading slash, or relative
* to the given {@code resourceLoadClass}.
* Example: <pre><code class='java'>
* // Check that the response is strictly equal to the content of
* // the specified file:
* assertThat(...).body().json(PersonController.class)
* .isStrictlyEqualToJson("person-created.json");
* </code></pre>
* @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);
}
/**
* Verifies that the response body is equal to the given {@link String}.
* <p>Convert 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 {@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);
}
}

9
spring-test/src/main/java/org/springframework/test/web/servlet/assertj/package-info.java

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
/**
* AssertJ support for MockMvc.
*/
@NonNullApi
@NonNullFields
package org.springframework.test.web.servlet.assertj;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

190
spring-test/src/test/java/org/springframework/test/http/HttpHeadersAssertTests.java

@ -0,0 +1,190 @@ @@ -0,0 +1,190 @@
/*
* 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.http;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Tests for {@link HttpHeadersAssert}.
*
* @author Stephane Nicoll
*/
class HttpHeadersAssertTests {
@Test
void containsHeader() {
assertThat(Map.of("first", "1")).containsHeader("first");
}
@Test
void containsHeaderWithNameNotPresent() {
Map<String, String> map = Map.of("first", "1");
Assertions.assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(map).containsHeader("wrong-name"))
.withMessageContainingAll("HTTP headers", "first", "wrong-name");
}
@Test
void containsHeaders() {
assertThat(Map.of("first", "1", "second", "2", "third", "3"))
.containsHeaders("first", "third");
}
@Test
void containsHeadersWithSeveralNamesNotPresent() {
Map<String, String> map = Map.of("first", "1", "second", "2", "third", "3");
Assertions.assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(map).containsHeaders("first", "wrong-name", "another-wrong-name", "third"))
.withMessageContainingAll("HTTP headers", "first", "wrong-name", "another-wrong-name");
}
@Test
void doesNotContainsHeader() {
assertThat(Map.of("first", "1")).doesNotContainsHeader("second");
}
@Test
void doesNotContainsHeaderWithNamePresent() {
Map<String, String> map = Map.of("first", "1");
Assertions.assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(map).doesNotContainKey("first"))
.withMessageContainingAll("HTTP headers", "first");
}
@Test
void doesNotContainsHeaders() {
assertThat(Map.of("first", "1", "third", "3"))
.doesNotContainsHeaders("second", "fourth");
}
@Test
void doesNotContainsHeadersWithSeveralNamesPresent() {
Map<String, String> map = Map.of("first", "1", "second", "2", "third", "3");
Assertions.assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(map).doesNotContainsHeaders("first", "another-wrong-name", "second"))
.withMessageContainingAll("HTTP headers", "first", "second");
}
@Test
void hasValueWithStringMatch() {
HttpHeaders headers = new HttpHeaders();
headers.addAll("header", List.of("a", "b", "c"));
assertThat(headers).hasValue("header", "a");
}
@Test
void hasValueWithStringMatchOnSecondaryValue() {
HttpHeaders headers = new HttpHeaders();
headers.addAll("header", List.of("first", "second", "third"));
Assertions.assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(headers).hasValue("header", "second"))
.withMessageContainingAll("check primary value for HTTP header 'header'", "first", "second");
}
@Test
void hasValueWithNoStringMatch() {
HttpHeaders headers = new HttpHeaders();
headers.addAll("header", List.of("first", "second", "third"));
Assertions.assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(headers).hasValue("wrong-name", "second"))
.withMessageContainingAll("HTTP headers", "header", "wrong-name");
}
@Test
void hasValueWithNonPresentHeader() {
HttpHeaders map = new HttpHeaders();
map.add("test-header", "a");
Assertions.assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(map).hasValue("wrong-name", "a"))
.withMessageContainingAll("HTTP headers", "test-header", "wrong-name");
}
@Test
void hasValueWithLongMatch() {
HttpHeaders headers = new HttpHeaders();
headers.addAll("header", List.of("123", "456", "789"));
assertThat(headers).hasValue("header", 123);
}
@Test
void hasValueWithLongMatchOnSecondaryValue() {
HttpHeaders map = new HttpHeaders();
map.addAll("header", List.of("123", "456", "789"));
Assertions.assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(map).hasValue("header", 456))
.withMessageContainingAll("check primary long value for HTTP header 'header'", "123", "456");
}
@Test
void hasValueWithNoLongMatch() {
HttpHeaders map = new HttpHeaders();
map.addAll("header", List.of("123", "456", "789"));
Assertions.assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(map).hasValue("wrong-name", 456))
.withMessageContainingAll("HTTP headers", "header", "wrong-name");
}
@Test
void hasValueWithInstantMatch() {
Instant instant = Instant.now();
HttpHeaders headers = new HttpHeaders();
headers.setInstant("header", instant);
assertThat(headers).hasValue("header", instant);
}
@Test
void hasValueWithNoInstantMatch() {
Instant instant = Instant.now();
HttpHeaders map = new HttpHeaders();
map.setInstant("header", instant);
Assertions.assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(map).hasValue("wrong-name", instant.minusSeconds(30)))
.withMessageContainingAll("HTTP headers", "header", "wrong-name");
}
@Test
void hasValueWithNoInstantMatchOneSecOfDifference() {
Instant instant = Instant.now();
HttpHeaders map = new HttpHeaders();
map.setInstant("header", instant);
Assertions.assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(map).hasValue("wrong-name", instant.minusSeconds(1)))
.withMessageContainingAll("HTTP headers", "header", "wrong-name");
}
private static HttpHeadersAssert assertThat(Map<String, String> values) {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
values.forEach(map::add);
return assertThat(new HttpHeaders(map));
}
private static HttpHeadersAssert assertThat(HttpHeaders values) {
return new HttpHeadersAssert(values);
}
}

157
spring-test/src/test/java/org/springframework/test/http/MediaTypeAssertTests.java

@ -0,0 +1,157 @@ @@ -0,0 +1,157 @@
/*
* 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.http;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link MediaTypeAssert}.
*
* @author Brian Clozel
* @author Stephane Nicoll
*/
class MediaTypeAssertTests {
@Test
void actualCanBeNull() {
new MediaTypeAssert((MediaType) null).isNull();
}
@Test
void actualStringCanBeNull() {
new MediaTypeAssert((String) null).isNull();
}
@Test
void isEqualWhenSameShouldPass() {
assertThat(mediaType("application/json")).isEqualTo("application/json");
}
@Test
void isEqualWhenDifferentShouldFail() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(mediaType("application/json")).isEqualTo("text/html"))
.withMessageContaining("Media type");
}
@Test
void isEqualWhenActualIsNullShouldFail() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(null).isEqualTo(MediaType.APPLICATION_JSON))
.withMessageContaining("Media type");
}
@Test
void isEqualWhenSameTypeShouldPass() {
assertThat(mediaType("application/json")).isEqualTo(MediaType.APPLICATION_JSON);
}
@Test
void isEqualWhenDifferentTypeShouldFail() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(mediaType("application/json")).isEqualTo(MediaType.TEXT_HTML))
.withMessageContaining("Media type");
}
@Test
void isCompatibleWhenSameShouldPass() {
assertThat(mediaType("application/json")).isCompatibleWith("application/json");
}
@Test
void isCompatibleWhenCompatibleShouldPass() {
assertThat(mediaType("application/json")).isCompatibleWith("application/*");
}
@Test
void isCompatibleWhenDifferentShouldFail() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(mediaType("application/json")).isCompatibleWith("text/html"))
.withMessageContaining("check media type 'application/json' is compatible with 'text/html'");
}
@Test
void isCompatibleWithStringAndNullActual() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(null).isCompatibleWith("text/html"))
.withMessageContaining("Expecting null to be compatible with 'text/html'");
}
@Test
void isCompatibleWithStringAndNullExpected() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(mediaType("application/json")).isCompatibleWith((String) null))
.withMessageContainingAll("Expecting:", "null", "To be a valid media type but got:",
"'mimeType' must not be empty");
}
@Test
void isCompatibleWithStringAndEmptyExpected() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(mediaType("application/json")).isCompatibleWith(""))
.withMessageContainingAll("Expecting:", "", "To be a valid media type but got:",
"'mimeType' must not be empty");
}
@Test
void isCompatibleWithMediaTypeAndNullActual() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(null).isCompatibleWith(MediaType.TEXT_HTML))
.withMessageContaining("Expecting null to be compatible with 'text/html'");
}
@Test
void isCompatibleWithMediaTypeAndNullExpected() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(mediaType("application/json")).isCompatibleWith((MediaType) null))
.withMessageContaining("Expecting 'application/json' to be compatible with null");
}
@Test
void isCompatibleWhenSameTypeShouldPass() {
assertThat(mediaType("application/json")).isCompatibleWith(MediaType.APPLICATION_JSON);
}
@Test
void isCompatibleWhenCompatibleTypeShouldPass() {
assertThat(mediaType("application/json")).isCompatibleWith(MediaType.parseMediaType("application/*"));
}
@Test
void isCompatibleWhenDifferentTypeShouldFail() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(mediaType("application/json")).isCompatibleWith(MediaType.TEXT_HTML))
.withMessageContaining("check media type 'application/json' is compatible with 'text/html'");
}
@Nullable
private static MediaType mediaType(@Nullable String mediaType) {
return (mediaType != null ? MediaType.parseMediaType(mediaType) : null);
}
private static MediaTypeAssert assertThat(@Nullable MediaType mediaType) {
return new MediaTypeAssert(mediaType);
}
}

479
spring-test/src/test/java/org/springframework/test/json/JsonContentAssertTests.java

@ -0,0 +1,479 @@ @@ -0,0 +1,479 @@
/*
* 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<Arguments> 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<Arguments> 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<Arguments> 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<JsonContentAssert> forJson(@Nullable String json) {
return () -> new JsonContentAssert(json, JsonContentAssertTests.class);
}
}

60
spring-test/src/test/java/org/springframework/test/json/JsonContentTests.java

@ -0,0 +1,60 @@ @@ -0,0 +1,60 @@
/*
* 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 org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link JsonContent}.
*
* @author Phillip Webb
*/
class JsonContentTests {
private static final String JSON = "{\"name\":\"spring\", \"age\":100}";
@Test
void createWhenJsonIsNullShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(
() -> new JsonContent(null, null))
.withMessageContaining("JSON must not be null");
}
@Test
@SuppressWarnings("deprecation")
void assertThatShouldReturnJsonContentAssert() {
JsonContent content = new JsonContent(JSON, getClass());
assertThat(content.assertThat()).isInstanceOf(JsonContentAssert.class);
}
@Test
void getJsonShouldReturnJson() {
JsonContent content = new JsonContent(JSON, getClass());
assertThat(content.getJson()).isEqualTo(JSON);
}
@Test
void toStringShouldReturnString() {
JsonContent content = new JsonContent(JSON, getClass());
assertThat(content.toString()).isEqualTo("JsonContent " + JSON);
}
}

322
spring-test/src/test/java/org/springframework/test/json/JsonPathAssertTests.java

@ -0,0 +1,322 @@ @@ -0,0 +1,322 @@
/*
* 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<Arguments> 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<List<Member>>() {})
.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<AssertionError> hasFailedToMatchPath(String expression) {
return error -> assertThat(error.getMessage()).containsSubsequence("Expecting:",
"To match JSON path:", "\"" + expression + "\"");
}
private Consumer<AssertionError> hasFailedToNotMatchPath(String expression) {
return error -> assertThat(error.getMessage()).containsSubsequence("Expecting:",
"To not 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<JsonPathAssert> forJson(String json) {
return forJson(json, null);
}
private AssertProvider<JsonPathAssert> forJson(String json,
@Nullable GenericHttpMessageConverter<Object> jsonHttpMessageConverter) {
return () -> new JsonPathAssert(json, jsonHttpMessageConverter);
}
}

333
spring-test/src/test/java/org/springframework/test/json/JsonPathValueAssertTests.java

@ -0,0 +1,333 @@ @@ -0,0 +1,333 @@
/*
* 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.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.assertj.core.api.AssertProvider;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.assertj.core.data.Offset;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
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 JsonPathValueAssert}.
*
* @author Stephane Nicoll
*/
class JsonPathValueAssertTests {
@Nested
class AsStringTests {
@Test
void asStringWithStringValue() {
assertThat(forValue("test")).asString().isEqualTo("test");
}
@Test
void asStringWithEmptyValue() {
assertThat(forValue("")).asString().isEmpty();
}
@Test
void asStringWithNonStringFails() {
int value = 123;
AssertProvider<JsonPathValueAssert> actual = forValue(value);
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(actual).asString().isEqualTo("123"))
.satisfies(hasFailedToBeOfType(value, "a string"));
}
@Test
void asStringWithNullFails() {
AssertProvider<JsonPathValueAssert> actual = forValue(null);
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(actual).asString().isEqualTo("null"))
.satisfies(hasFailedToBeOfTypeWhenNull("a string"));
}
}
@Nested
class AsNumberTests {
@Test
void asNumberWithIntegerValue() {
assertThat(forValue(123)).asNumber().isEqualTo(123);
}
@Test
void asNumberWithDoubleValue() {
assertThat(forValue(3.1415926)).asNumber()
.asInstanceOf(InstanceOfAssertFactories.DOUBLE)
.isEqualTo(3.14, Offset.offset(0.01));
}
@Test
void asNumberWithNonNumberFails() {
String value = "123";
AssertProvider<JsonPathValueAssert> actual = forValue(value);
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(actual).asNumber().isEqualTo(123))
.satisfies(hasFailedToBeOfType(value, "a number"));
}
@Test
void asNumberWithNullFails() {
AssertProvider<JsonPathValueAssert> actual = forValue(null);
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(actual).asNumber().isEqualTo(0))
.satisfies(hasFailedToBeOfTypeWhenNull("a number"));
}
}
@Nested
class AsBooleanTests {
@Test
void asBooleanWithBooleanPrimitiveValue() {
assertThat(forValue(true)).asBoolean().isEqualTo(true);
}
@Test
void asBooleanWithBooleanWrapperValue() {
assertThat(forValue(Boolean.FALSE)).asBoolean().isEqualTo(false);
}
@Test
void asBooleanWithNonBooleanFails() {
String value = "false";
AssertProvider<JsonPathValueAssert> actual = forValue(value);
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(actual).asBoolean().isEqualTo(false))
.satisfies(hasFailedToBeOfType(value, "a boolean"));
}
@Test
void asBooleanWithNullFails() {
AssertProvider<JsonPathValueAssert> actual = forValue(null);
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(actual).asBoolean().isEqualTo(false))
.satisfies(hasFailedToBeOfTypeWhenNull("a boolean"));
}
}
@Nested
class AsArrayTests { // json path uses List for arrays
@Test
void asArrayWithStringValues() {
assertThat(forValue(List.of("a", "b", "c"))).asArray().contains("a", "c");
}
@Test
void asArrayWithEmptyArray() {
assertThat(forValue(Collections.emptyList())).asArray().isEmpty();
}
@Test
void asArrayWithNonArrayFails() {
String value = "test";
AssertProvider<JsonPathValueAssert> actual = forValue(value);
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(actual).asArray().contains("t"))
.satisfies(hasFailedToBeOfType(value, "an array"));
}
@Test
void asArrayWithNullFails() {
AssertProvider<JsonPathValueAssert> actual = forValue(null);
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(actual).asArray().isEqualTo(false))
.satisfies(hasFailedToBeOfTypeWhenNull("an array"));
}
}
@Nested
class AsMapTests {
@Test
void asMapWithMapValue() {
assertThat(forValue(Map.of("zero", 0, "one", 1))).asMap().containsKeys("zero", "one")
.containsValues(0, 1);
}
@Test
void asArrayWithEmptyMap() {
assertThat(forValue(Collections.emptyMap())).asMap().isEmpty();
}
@Test
void asMapWithNonMapFails() {
List<String> value = List.of("a", "b");
AssertProvider<JsonPathValueAssert> actual = forValue(value);
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(actual).asMap().containsKey("a"))
.satisfies(hasFailedToBeOfType(value, "a map"));
}
@Test
void asMapWithNullFails() {
AssertProvider<JsonPathValueAssert> actual = forValue(null);
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(actual).asMap().isEmpty())
.satisfies(hasFailedToBeOfTypeWhenNull("a map"));
}
}
@Nested
class ConvertToTests {
private static final MappingJackson2HttpMessageConverter jsonHttpMessageConverter =
new MappingJackson2HttpMessageConverter(new ObjectMapper());
@Test
void convertToWithoutHttpMessageConverter() {
AssertProvider<JsonPathValueAssert> actual = () -> new JsonPathValueAssert("123", "$.test", null);
assertThatIllegalStateException().isThrownBy(() -> assertThat(actual).convertTo(Integer.class))
.withMessage("No JSON message converter available to convert '123'");
}
@Test
void convertObjectToPojo() {
assertThat(forValue(Map.of("id", 1234, "name", "John", "active", true))).convertTo(User.class)
.satisfies(user -> {
assertThat(user.id).isEqualTo(1234);
assertThat(user.name).isEqualTo("John");
assertThat(user.active).isTrue();
});
}
@Test
void convertArrayToListOfPojo() {
Map<?, ?> user1 = Map.of("id", 1234, "name", "John", "active", true);
Map<?, ?> user2 = Map.of("id", 5678, "name", "Sarah", "active", false);
Map<?, ?> user3 = Map.of("id", 9012, "name", "Sophia", "active", true);
assertThat(forValue(List.of(user1, user2, user3)))
.convertTo(new ParameterizedTypeReference<List<User>>() {})
.satisfies(users -> assertThat(users).hasSize(3).extracting("name")
.containsExactly("John", "Sarah", "Sophia"));
}
@Test
void convertObjectToPojoWithMissingMandatoryField() {
Map<?, ?> value = Map.of("firstName", "John");
AssertProvider<JsonPathValueAssert> actual = forValue(value);
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(actual).convertTo(User.class))
.satisfies(hasFailedToConvertToType(value, User.class))
.withMessageContaining("firstName");
}
private AssertProvider<JsonPathValueAssert> forValue(@Nullable Object actual) {
return () -> new JsonPathValueAssert(actual, "$.test", jsonHttpMessageConverter);
}
private record User(long id, String name, boolean active) {}
}
@Nested
class EmptyNotEmptyTests {
@Test
void isEmptyWithEmptyString() {
assertThat(forValue("")).isEmpty();
}
@Test
void isEmptyWithNull() {
assertThat(forValue(null)).isEmpty();
}
@Test
void isEmptyWithEmptyArray() {
assertThat(forValue(Collections.emptyList())).isEmpty();
}
@Test
void isEmptyWithEmptyObject() {
assertThat(forValue(Collections.emptyMap())).isEmpty();
}
@Test
void isEmptyWithWhitespace() {
AssertProvider<JsonPathValueAssert> actual = forValue(" ");
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(actual).isEmpty())
.satisfies(hasFailedEmptyCheck(" "));
}
@Test
void isNotEmptyWithString() {
assertThat(forValue("test")).isNotEmpty();
}
@Test
void isNotEmptyWithArray() {
assertThat(forValue(List.of("test"))).isNotEmpty();
}
@Test
void isNotEmptyWithObject() {
assertThat(forValue(Map.of("test", "value"))).isNotEmpty();
}
private Consumer<AssertionError> hasFailedEmptyCheck(Object actual) {
return error -> assertThat(error.getMessage()).containsSubsequence("Expected value at JSON path \"$.test\":",
"" + StringUtils.quoteIfString(actual), "To be empty");
}
}
private Consumer<AssertionError> hasFailedToBeOfType(Object actual, String expectedDescription) {
return error -> assertThat(error.getMessage()).containsSubsequence("Expected value at JSON path \"$.test\":",
"" + StringUtils.quoteIfString(actual), "To be " + expectedDescription, "But was:", actual.getClass().getName());
}
private Consumer<AssertionError> hasFailedToBeOfTypeWhenNull(String expectedDescription) {
return error -> assertThat(error.getMessage()).containsSubsequence("Expected value at JSON path \"$.test\":", "null",
"To be " + expectedDescription);
}
private Consumer<AssertionError> hasFailedToConvertToType(Object actual, Class<?> targetType) {
return error -> assertThat(error.getMessage()).containsSubsequence("Expected value at JSON path \"$.test\":",
"" + StringUtils.quoteIfString(actual), "To convert successfully to:", targetType.getTypeName(), "But it failed:");
}
private AssertProvider<JsonPathValueAssert> forValue(@Nullable Object actual) {
return () -> new JsonPathValueAssert(actual, "$.test", null);
}
}

92
spring-test/src/test/java/org/springframework/test/util/MethodAssertTests.java

@ -0,0 +1,92 @@ @@ -0,0 +1,92 @@
/*
* 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.util;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link MethodAssert}.
*
* @author Stephane Nicoll
*/
class MethodAssertTests {
@Test
void isEqualTo() {
Method method = ReflectionUtils.findMethod(TestData.class, "counter");
assertThat(method).isEqualTo(method);
}
@Test
void hasName() {
assertThat(ReflectionUtils.findMethod(TestData.class, "counter")).hasName("counter");
}
@Test
void hasNameWithWrongName() {
Method method = ReflectionUtils.findMethod(TestData.class, "counter");
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(method).hasName("invalid"))
.withMessageContainingAll("Method name", "counter", "invalid");
}
@Test
void hasNameWithNullMethod() {
Method method = ReflectionUtils.findMethod(TestData.class, "notAMethod");
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(method).hasName("name"))
.withMessageContaining("Expecting actual not to be null");
}
@Test
void hasDeclaringClass() {
assertThat(ReflectionUtils.findMethod(TestData.class, "counter")).hasDeclaringClass(TestData.class);
}
@Test
void haDeclaringClassWithWrongClass() {
Method method = ReflectionUtils.findMethod(TestData.class, "counter");
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(method).hasDeclaringClass(Method.class))
.withMessageContainingAll("Method declaring class",
TestData.class.getCanonicalName(), Method.class.getCanonicalName());
}
@Test
void hasDeclaringClassWithNullMethod() {
Method method = ReflectionUtils.findMethod(TestData.class, "notAMethod");
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(method).hasDeclaringClass(TestData.class))
.withMessageContaining("Expecting actual not to be null");
}
private MethodAssert assertThat(@Nullable Method method) {
return new MethodAssert(method);
}
record TestData(String name, int counter) {}
}

136
spring-test/src/test/java/org/springframework/test/validation/AbstractBindingResultAssertTests.java

@ -0,0 +1,136 @@ @@ -0,0 +1,136 @@
/*
* 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.validation;
import java.util.Map;
import org.assertj.core.api.AssertProvider;
import org.junit.jupiter.api.Test;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link AbstractBindingResultAssert}.
*
* @author Stephane Nicoll
*/
class AbstractBindingResultAssertTests {
@Test
void hasErrorsCountWithNoError() {
assertThat(bindingResult(new TestBean(), Map.of("name", "John", "age", "42"))).hasErrorsCount(0);
}
@Test
void hasErrorsCountWithInvalidCount() {
AssertProvider<BindingResultAssert> actual = bindingResult(new TestBean(),
Map.of("name", "John", "age", "4x", "touchy", "invalid.value"));
assertThatExceptionOfType(AssertionError.class).isThrownBy(
() -> assertThat(actual).hasErrorsCount(1))
.withMessageContainingAll("check errors for attribute 'test'", "1", "2");
}
@Test
void hasFieldErrorsWithMatchingSubset() {
assertThat(bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y")))
.hasFieldErrors("touchy");
}
@Test
void hasFieldErrorsWithAllMatching() {
assertThat(bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y")))
.hasFieldErrors("touchy", "age");
}
@Test
void hasFieldErrorsWithNotAllMatching() {
AssertProvider<BindingResultAssert> actual = bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y"));
assertThatExceptionOfType(AssertionError.class).isThrownBy(
() -> assertThat(actual).hasFieldErrors("age", "name"))
.withMessageContainingAll("check field errors", "age", "touchy", "name");
}
@Test
void hasOnlyFieldErrorsWithAllMatching() {
assertThat(bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y")))
.hasOnlyFieldErrors("touchy", "age");
}
@Test
void hasOnlyFieldErrorsWithMatchingSubset() {
AssertProvider<BindingResultAssert> actual = bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y"));
assertThatExceptionOfType(AssertionError.class).isThrownBy(
() -> assertThat(actual).hasOnlyFieldErrors("age"))
.withMessageContainingAll("check field errors", "age", "touchy");
}
@Test
void hasFieldErrorCodeWithMatchingCode() {
assertThat(bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y")))
.hasFieldErrorCode("age", "typeMismatch");
}
@Test
void hasFieldErrorCodeWitNonMatchingCode() {
AssertProvider<BindingResultAssert> actual = bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y"));
assertThatExceptionOfType(AssertionError.class).isThrownBy(
() -> assertThat(actual).hasFieldErrorCode("age", "castFailure"))
.withMessageContainingAll("check error code for field 'age'", "castFailure", "typeMismatch");
}
@Test
void hasFieldErrorCodeWitNonMatchingField() {
AssertProvider<BindingResultAssert> actual = bindingResult(new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "x.y"));
assertThatExceptionOfType(AssertionError.class).isThrownBy(
() -> assertThat(actual).hasFieldErrorCode("unknown", "whatever"))
.withMessageContainingAll("Expecting binding result", "touchy", "age",
"to have at least an error for field 'unknown'");
}
private AssertProvider<BindingResultAssert> bindingResult(Object instance, Map<String, Object> propertyValues) {
return () -> new BindingResultAssert("test", createBindingResult(instance, propertyValues));
}
private static BindingResult createBindingResult(Object instance, Map<String, Object> propertyValues) {
DataBinder binder = new DataBinder(instance, "test");
MutablePropertyValues pvs = new MutablePropertyValues(propertyValues);
binder.bind(pvs);
try {
binder.close();
return binder.getBindingResult();
}
catch (BindException ex) {
return ex.getBindingResult();
}
}
private static final class BindingResultAssert extends AbstractBindingResultAssert<BindingResultAssert> {
public BindingResultAssert(String name, BindingResult bindingResult) {
super(name, bindingResult, BindingResultAssert.class);
}
}
}

77
spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java

@ -0,0 +1,77 @@ @@ -0,0 +1,77 @@
/*
* 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;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link UriAssert}.
*
* @author Stephane Nicoll
*/
class UriAssertTests {
@Test
void isEqualToTemplate() {
assertThat("/orders/1/items/2").isEqualToTemplate("/orders/{orderId}/items/{itemId}", 1, 2);
}
@Test
void isEqualToTemplateWithWrongValue() {
String expected = "/orders/1/items/3";
String actual = "/orders/1/items/2";
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(expected).isEqualToTemplate("/orders/{orderId}/items/{itemId}", 1, 2))
.withMessageContainingAll("Test URI", expected, actual);
}
@Test
void isEqualToTemplateMissingArg() {
String template = "/orders/{orderId}/items/{itemId}";
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat("/orders/1/items/2").isEqualToTemplate(template, 1))
.withMessageContainingAll("Expecting:", template,
"Not enough variable values available to expand 'itemId'");
}
@Test
void matchPattern() {
assertThat("/orders/1").matchPattern("/orders/*");
}
@Test
void matchPatternWithNonValidPattern() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat("/orders/1").matchPattern("/orders/"))
.withMessage("'/orders/' is not an Ant-style path pattern");
}
@Test
void matchPatternWithWrongValue() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat("/orders/1").matchPattern("/resources/*"))
.withMessageContainingAll("Test URI", "/resources/*", "/orders/1");
}
UriAssert assertThat(String uri) {
return new UriAssert(uri, "Test URI");
}
}

143
spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssertTests.java

@ -0,0 +1,143 @@ @@ -0,0 +1,143 @@
/*
* 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.util.LinkedHashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import static java.util.Map.entry;
/**
* Tests for {@link AbstractHttpServletRequestAssert}.
*
* @author Stephane Nicoll
*/
public class AbstractHttpServletRequestAssertTests {
@Nested
class AttributesTests {
@Test
void attributesAreCopied() {
Map<String, Object> map = new LinkedHashMap<>();
map.put("one", 1);
map.put("two", 2);
assertThat(createRequest(map)).attributes()
.containsExactly(entry("one", 1), entry("two", 2));
}
@Test
void attributesWithWrongKey() {
HttpServletRequest request = createRequest(Map.of("one", 1));
Assertions.assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(request).attributes().containsKey("two"))
.withMessageContainingAll("Request Attributes", "two", "one");
}
private HttpServletRequest createRequest(Map<String, Object> attributes) {
MockHttpServletRequest request = new MockHttpServletRequest();
attributes.forEach(request::setAttribute);
return request;
}
}
@Nested
class SessionAttributesTests {
@Test
void sessionAttributesAreCopied() {
Map<String, Object> map = new LinkedHashMap<>();
map.put("one", 1);
map.put("two", 2);
assertThat(createRequest(map)).sessionAttributes()
.containsExactly(entry("one", 1), entry("two", 2));
}
@Test
void sessionAttributesWithWrongKey() {
HttpServletRequest request = createRequest(Map.of("one", 1));
Assertions.assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(request).sessionAttributes().containsKey("two"))
.withMessageContainingAll("Session Attributes", "two", "one");
}
private HttpServletRequest createRequest(Map<String, Object> attributes) {
MockHttpServletRequest request = new MockHttpServletRequest();
HttpSession session = request.getSession();
Assertions.assertThat(session).isNotNull();
attributes.forEach(session::setAttribute);
return request;
}
}
@Test
void hasAsyncStartedTrue() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAsyncStarted(true);
assertThat(request).hasAsyncStarted(true);
}
@Test
void hasAsyncStartedTrueWithFalse() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAsyncStarted(false);
Assertions.assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(request).hasAsyncStarted(true))
.withMessage("Async expected to have started");
}
@Test
void hasAsyncStartedFalse() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAsyncStarted(false);
assertThat(request).hasAsyncStarted(false);
}
@Test
void hasAsyncStartedFalseWithTrue() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAsyncStarted(true);
Assertions.assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(request).hasAsyncStarted(false))
.withMessage("Async expected to not have started");
}
private static ResponseAssert assertThat(HttpServletRequest response) {
return new ResponseAssert(response);
}
private static final class ResponseAssert extends AbstractHttpServletRequestAssert<ResponseAssert, HttpServletRequest> {
ResponseAssert(HttpServletRequest actual) {
super(actual, ResponseAssert.class);
}
}
}

138
spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssertTests.java

@ -0,0 +1,138 @@ @@ -0,0 +1,138 @@
/*
* 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.util.Map;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletResponse;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link AbstractHttpServletResponseAssert}.
*
* @author Stephane Nicoll
*/
class AbstractHttpServletResponseAssertTests {
@Nested
class HeadersTests {
@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<String, String> headers) {
MockHttpServletResponse response = new MockHttpServletResponse();
headers.forEach(response::addHeader);
return response;
}
}
@Nested
class StatusTests {
@Test
void hasStatusWithCode() {
assertThat(createResponse(200)).hasStatus(200);
}
@Test
void hasStatusWithHttpStatus() {
assertThat(createResponse(200)).hasStatus(HttpStatus.OK);
}
@Test
void hasStatusOK() {
assertThat(createResponse(200)).hasStatusOk();
}
@Test
void hasStatusWithWrongCode() {
MockHttpServletResponse response = createResponse(200);
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(response).hasStatus(300))
.withMessageContainingAll("HTTP status code", "200", "300");
}
@Test
void hasStatus1xxInformational() {
assertThat(createResponse(199)).hasStatus1xxInformational();
}
@Test
void hasStatus2xxSuccessful() {
assertThat(createResponse(299)).hasStatus2xxSuccessful();
}
@Test
void hasStatus3xxRedirection() {
assertThat(createResponse(399)).hasStatus3xxRedirection();
}
@Test
void hasStatus4xxClientError() {
assertThat(createResponse(499)).hasStatus4xxClientError();
}
@Test
void hasStatus5xxServerError() {
assertThat(createResponse(599)).hasStatus5xxServerError();
}
@Test
void hasStatusWithWrongSeries() {
MockHttpServletResponse response = createResponse(500);
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(response).hasStatus2xxSuccessful())
.withMessageContainingAll("HTTP status series", "SUCCESSFUL", "SERVER_ERROR");
}
private MockHttpServletResponse createResponse(int status) {
MockHttpServletResponse response = new MockHttpServletResponse();
response.setStatus(status);
return response;
}
}
private static ResponseAssert assertThat(HttpServletResponse response) {
return new ResponseAssert(response);
}
private static final class ResponseAssert extends AbstractHttpServletResponseAssert<HttpServletResponse, ResponseAssert, HttpServletResponse> {
ResponseAssert(HttpServletResponse actual) {
super(actual, ResponseAssert.class);
}
@Override
protected HttpServletResponse getResponse() {
return this.actual;
}
}
}

48
spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssertTests.java

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
/*
* 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 org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
/**
* Tests for {@link AbstractMockHttpServletRequestAssert}.
*
* @author Stephane Nicoll
*/
class AbstractMockHttpServletRequestAssertTests {
@Test
void requestCanBeAsserted() {
MockHttpServletRequest request = new MockHttpServletRequest();
assertThat(request).satisfies(actual -> assertThat(actual).isSameAs(request));
}
private static RequestAssert assertThat(MockHttpServletRequest request) {
return new RequestAssert(request);
}
private static final class RequestAssert extends AbstractMockHttpServletRequestAssert<RequestAssert> {
RequestAssert(MockHttpServletRequest actual) {
super(actual, RequestAssert.class);
}
}
}

106
spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java

@ -0,0 +1,106 @@ @@ -0,0 +1,106 @@
/*
* 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.StandardCharsets;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.web.MockHttpServletResponse;
/**
* Tests for {@link AbstractMockHttpServletResponseAssert}.
*
* @author Stephane Nicoll
*/
public class AbstractMockHttpServletResponseAssertTests {
@Test
void hasForwardedUrl() {
String forwardedUrl = "https://example.com/42";
MockHttpServletResponse response = new MockHttpServletResponse();
response.setForwardedUrl(forwardedUrl);
assertThat(response).hasForwardedUrl(forwardedUrl);
}
@Test
void hasForwardedUrlWithWrongValue() {
String forwardedUrl = "https://example.com/42";
MockHttpServletResponse response = new MockHttpServletResponse();
response.setForwardedUrl(forwardedUrl);
Assertions.assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(response).hasForwardedUrl("another"))
.withMessageContainingAll("Forwarded URL", forwardedUrl, "another");
}
@Test
void hasRedirectedUrl() {
String redirectedUrl = "https://example.com/42";
MockHttpServletResponse response = new MockHttpServletResponse();
response.addHeader(HttpHeaders.LOCATION, redirectedUrl);
assertThat(response).hasRedirectedUrl(redirectedUrl);
}
@Test
void hasRedirectedUrlWithWrongValue() {
String redirectedUrl = "https://example.com/42";
MockHttpServletResponse response = new MockHttpServletResponse();
response.addHeader(HttpHeaders.LOCATION, redirectedUrl);
Assertions.assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(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");
}
@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 final class ResponseAssert extends AbstractMockHttpServletResponseAssert<ResponseAssert, MockHttpServletResponse> {
ResponseAssert(MockHttpServletResponse actual) {
super(null, actual, ResponseAssert.class);
}
@Override
protected MockHttpServletResponse getResponse() {
return this.actual;
}
}
}

558
spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcIntegrationTests.java

@ -0,0 +1,558 @@ @@ -0,0 +1,558 @@
/*
* 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.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Size;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
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;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import static java.util.Map.entry;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.InstanceOfAssertFactories.map;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Integration tests for {@link AssertableMockMvc}.
*
* @author Brian Clozel
* @author Stephane Nicoll
*/
@SpringJUnitConfig
@WebAppConfiguration
public class AssertableMockMvcIntegrationTests {
private final AssertableMockMvc mockMvc;
AssertableMockMvcIntegrationTests(WebApplicationContext wac) {
this.mockMvc = AssertableMockMvc.from(wac);
}
@Nested
class RequestTests {
@Test
void hasAsyncStartedTrue() {
assertThat(perform(get("/callable").accept(MediaType.APPLICATION_JSON)))
.request().hasAsyncStarted(true);
}
@Test
void hasAsyncStartedFalse() {
assertThat(perform(get("/greet"))).request().hasAsyncStarted(false);
}
@Test
void attributes() {
assertThat(perform(get("/greet"))).request().attributes()
.containsKey(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}
@Test
void sessionAttributes() {
assertThat(perform(get("/locale"))).request().sessionAttributes()
.containsOnly(entry("locale", Locale.UK));
}
}
@Nested
class CookieTests {
@Test
void containsCookie() {
Cookie cookie = new Cookie("test", "value");
assertThat(performWithCookie(cookie, get("/greet"))).cookies().containsCookie("test");
}
@Test
void hasValue() {
Cookie cookie = new Cookie("test", "value");
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(
new HandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
response.addCookie(cookie);
return true;
}
}).build());
return mockMvc.perform(request);
}
}
@Nested
class ContentTypeTests {
@Test
void contentType() {
assertThat(perform(get("/greet"))).contentType().isCompatibleWith("text/plain");
}
}
@Nested
class StatusTests {
@Test
void statusOk() {
assertThat(perform(get("/greet"))).hasStatusOk();
}
@Test
void statusSeries() {
assertThat(perform(get("/greet"))).hasStatus2xxSuccessful();
}
}
@Nested
class HeadersTests {
@Test
void shouldAssertHeader() {
assertThat(perform(get("/greet"))).headers()
.hasValue("Content-Type", "text/plain;charset=ISO-8859-1");
}
@Test
void shouldAssertHeaderWithCallback() {
assertThat(perform(get("/greet"))).headers().satisfies(textContent("ISO-8859-1"));
}
private Consumer<HttpHeaders> textContent(String charset) {
return headers -> assertThat(headers).containsEntry(
"Content-Type", List.of("text/plain;charset=%s".formatted(charset)));
}
}
@Nested
class ModelAndViewTests {
@Test
void hasViewName() {
assertThat(perform(get("/persons/{0}", "Andy"))).hasViewName("persons/index");
}
@Test
void viewNameWithCustomAssertion() {
assertThat(perform(get("/persons/{0}", "Andy"))).viewName().startsWith("persons");
}
@Test
void containsAttributes() {
assertThat(perform(post("/persons").param("name", "Andy"))).model()
.containsOnlyKeys("name").containsEntry("name", "Andy");
}
@Test
void hasErrors() {
assertThat(perform(post("/persons"))).model().hasErrors();
}
@Test
void hasAttributeErrors() {
assertThat(perform(post("/persons"))).model().hasAttributeErrors("person");
}
@Test
void hasAttributeErrorsCount() {
assertThat(perform(post("/persons"))).model().extractingBindingResult("person").hasErrorsCount(1);
}
}
@Nested
class FlashTests {
@Test
void containsAttributes() {
assertThat(perform(post("/persons").param("name", "Andy"))).flash()
.containsOnlyKeys("message").hasEntrySatisfying("message",
value -> assertThat(value).isInstanceOfSatisfying(String.class,
stringValue -> assertThat(stringValue).startsWith("success")));
}
}
@Nested
class BodyTests {
@Test
void asyncResult() {
assertThat(perform(get("/callable").accept(MediaType.APPLICATION_JSON)))
.asyncResult().asInstanceOf(map(String.class, Object.class))
.containsOnly(entry("key", "value"));
}
@Test
void stringContent() {
assertThat(perform(get("/greet"))).body().asString().isEqualTo("hello");
}
@Test
void jsonPathContent() {
assertThat(perform(get("/message"))).body().jsonPath()
.extractingPath("$.message").asString().isEqualTo("hello");
}
@Test
void jsonContentCanLoadResourceFromClasspath() {
assertThat(perform(get("/message"))).body().json().isLenientlyEqualTo(
new ClassPathResource("message.json", AssertableMockMvcIntegrationTests.class));
}
@Test
void jsonContentUsingResourceLoaderClass() {
assertThat(perform(get("/message"))).body().json(AssertableMockMvcIntegrationTests.class)
.isLenientlyEqualTo("message.json");
}
}
@Nested
class HandlerTests {
@Test
void handlerOn404() {
assertThat(perform(get("/unknown-resource"))).handler().isNull();
}
@Test
void hasType() {
assertThat(perform(get("/greet"))).handler().hasType(TestController.class);
}
@Test
void isMethodHandler() {
assertThat(perform(get("/greet"))).handler().isMethodHandler();
}
@Test
void isInvokedOn() {
assertThat(perform(get("/callable"))).handler()
.isInvokedOn(AsyncController.class, AsyncController::getCallable);
}
}
@Nested
class ExceptionTests {
@Test
void doesNotHaveUnresolvedException() {
assertThat(perform(get("/greet"))).doesNotHaveUnresolvedException();
}
@Test
void hasUnresolvedException() {
assertThat(perform(get("/error/1"))).hasUnresolvedException();
}
@Test
void doesNotHaveUnresolvedExceptionWithUnresolvedException() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertThat(perform(get("/error/1"))).doesNotHaveUnresolvedException())
.withMessage("Expecting request to have succeeded but it has failed");
}
@Test
void hasUnresolvedExceptionWithoutUnresolvedException() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertThat(perform(get("/greet"))).hasUnresolvedException())
.withMessage("Expecting request to have failed but it has succeeded");
}
@Test
void unresolvedExceptionWithFailedRequest() {
assertThat(perform(get("/error/1"))).unresolvedException()
.isInstanceOf(ServletException.class)
.cause().isInstanceOf(IllegalStateException.class).hasMessage("Expected");
}
@Test
void unresolvedExceptionWithSuccessfulRequest() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertThat(perform(get("/greet"))).unresolvedException())
.withMessage("Expecting request to have failed but it has succeeded");
}
// Check that assertions fail immediately if request has failed with unresolved exception
@Test
void assertAndApplyWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).apply(mvcResult -> {}));
}
@Test
void assertAsyncResultWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).asyncResult());
}
@Test
void assertContentTypeWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).contentType());
}
@Test
void assertCookiesWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).cookies());
}
@Test
void assertFlashWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).flash());
}
@Test
void assertStatusWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).hasStatus(3));
}
@Test
void assertHeaderWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).headers());
}
@Test
void assertViewNameWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).hasViewName("test"));
}
@Test
void assertForwardedUrlWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).hasForwardedUrl("test"));
}
@Test
void assertRedirectedUrlWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).hasRedirectedUrl("test"));
}
@Test
void assertRequestWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).request());
}
@Test
void assertModelWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).model());
}
@Test
void assertBodyWithUnresolvedException() {
testAssertionFailureWithUnresolvableException(
result -> assertThat(result).body());
}
private void testAssertionFailureWithUnresolvableException(Consumer<AssertableMvcResult> assertions) {
AssertableMvcResult result = perform(get("/error/1"));
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertions.accept(result))
.withMessageContainingAll("Request has failed unexpectedly:",
ServletException.class.getName(), IllegalStateException.class.getName(),
"Expected");
}
}
@Test
void hasForwardUrl() {
assertThat(perform(get("/persons/John"))).hasForwardedUrl("persons/index");
}
@Test
void hasRedirectUrl() {
assertThat(perform(post("/persons").param("name", "Andy"))).hasStatus(HttpStatus.FOUND)
.hasRedirectedUrl("/persons/Andy");
}
@Test
void satisfiesAllowAdditionalAssertions() {
assertThat(this.mockMvc.perform(get("/greet"))).satisfies(result -> {
assertThat(result).isInstanceOf(MvcResult.class);
assertThat(result).hasStatusOk();
});
}
@Test
void resultMatcherCanBeReused() {
assertThat(this.mockMvc.perform(get("/greet"))).matches(status().isOk());
}
@Test
void resultMatcherFailsWithDedicatedException() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(this.mockMvc.perform(get("/greet")))
.matches(status().isNotFound()))
.withMessageContaining("Status expected:<404> but was:<200>");
}
@Test
void shouldApplyResultHandler() { // Spring RESTDocs example
AtomicBoolean applied = new AtomicBoolean();
assertThat(this.mockMvc.perform(get("/greet"))).apply(result -> applied.set(true));
assertThat(applied).isTrue();
}
private AssertableMvcResult perform(MockHttpServletRequestBuilder builder) {
return this.mockMvc.perform(builder);
}
@Configuration
@EnableWebMvc
@Import({ TestController.class, PersonController.class, AsyncController.class,
SessionController.class, ErrorController.class })
static class WebConfiguration {
}
@RestController
static class TestController {
@GetMapping(path = "/greet", produces = "text/plain")
String greet() {
return "hello";
}
@GetMapping(path = "/message", produces = MediaType.APPLICATION_JSON_VALUE)
String message() {
return "{\"message\": \"hello\"}";
}
}
@Controller
@RequestMapping("/persons")
static class PersonController {
@GetMapping("/{name}")
public String get(@PathVariable String name, Model model) {
model.addAttribute(new Person(name));
return "persons/index";
}
@PostMapping
String create(@Valid Person person, Errors errors, RedirectAttributes redirectAttrs) {
if (errors.hasErrors()) {
return "persons/add";
}
redirectAttrs.addAttribute("name", person.getName());
redirectAttrs.addFlashAttribute("message", "success!");
return "redirect:/persons/{name}";
}
}
@RestController
static class AsyncController {
@GetMapping("/callable")
public Callable<Map<String, String>> getCallable() {
return () -> Collections.singletonMap("key", "value");
}
}
@Controller
@SessionAttributes("locale")
private static class SessionController {
@ModelAttribute
void populate(Model model) {
model.addAttribute("locale", Locale.UK);
}
@RequestMapping("/locale")
String handle() {
return "view";
}
}
@Controller
private static class ErrorController {
@GetMapping("/error/1")
public String one() {
throw new IllegalStateException("Expected");
}
@GetMapping("/error/validation/{id}")
public String validation(@PathVariable @Size(max = 4) String id) {
return "Hello " + id;
}
}
}

210
spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AssertableMockMvcTests.java

@ -0,0 +1,210 @@ @@ -0,0 +1,210 @@
/*
* 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.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockServletContext;
import org.springframework.test.json.JsonPathAssert;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.support.GenericWebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
/**
* Tests for {@link AssertableMockMvc}.
*
* @author Stephane Nicoll
*/
class AssertableMockMvcTests {
private static final MappingJackson2HttpMessageConverter jsonHttpMessageConverter =
new MappingJackson2HttpMessageConverter(new ObjectMapper());
@Test
void createShouldRejectNullMockMvc() {
assertThatThrownBy(() -> AssertableMockMvc.create(null))
.isInstanceOf(IllegalArgumentException.class);
}
@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");
}
}
@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");
}
@Test
void createWithControllersShouldUseThemAsIs() {
AssertableMockMvc mockMvc = AssertableMockMvc.of(new HelloController(),
new CounterController(new AtomicInteger(41)));
assertThat(mockMvc.perform(get("/hello"))).body().isEqualTo("Hello World");
assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 42");
assertThat(mockMvc.perform(post("/increase"))).body().isEqualTo("counter 43");
}
@Test
void createWithControllerAndCustomizations() {
AssertableMockMvc mockMvc = AssertableMockMvc.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();
assertThatIllegalStateException()
.isThrownBy(() -> jsonPathAssert.extractingPath("$").convertTo(Message.class))
.withMessageContaining("No JSON message converter available");
}
@Test
void createWithControllerCanConfigureHttpMessageConverters() {
AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class)
.withHttpMessageConverters(List.of(jsonHttpMessageConverter));
assertThat(mockMvc.perform(get("/json"))).hasStatusOk().body().jsonPath()
.extractingPath("$").convertTo(Message.class).satisfies(message -> {
assertThat(message.message()).isEqualTo("Hello World");
assertThat(message.counter()).isEqualTo(42);
});
}
@Test
@SuppressWarnings("unchecked")
void withHttpMessageConverterDetectsJsonConverter() {
MappingJackson2HttpMessageConverter converter = spy(jsonHttpMessageConverter);
AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class)
.withHttpMessageConverters(List.of(mock(GenericHttpMessageConverter.class),
mock(GenericHttpMessageConverter.class), converter));
assertThat(mockMvc.perform(get("/json"))).hasStatusOk().body().jsonPath()
.extractingPath("$").convertTo(Message.class).satisfies(message -> {
assertThat(message.message()).isEqualTo("Hello World");
assertThat(message.counter()).isEqualTo(42);
});
verify(converter).canWrite(Map.class, MediaType.APPLICATION_JSON);
}
@Test
void performWithUnresolvedExceptionSetsException() {
AssertableMockMvc mockMvc = AssertableMockMvc.of(HelloController.class);
AssertableMvcResult result = mockMvc.perform(get("/error"));
assertThat(result.getUnresolvedException()).isNotNull().isInstanceOf(ServletException.class)
.cause().isInstanceOf(IllegalStateException.class).hasMessage("Expected");
assertThat(result).hasFieldOrPropertyWithValue("target", null);
}
private GenericWebApplicationContext create(Class<?>... classes) {
GenericWebApplicationContext applicationContext = new GenericWebApplicationContext(
new MockServletContext());
AnnotationConfigUtils.registerAnnotationConfigProcessors(applicationContext);
for (Class<?> beanClass : classes) {
applicationContext.registerBean(beanClass);
}
applicationContext.refresh();
return applicationContext;
}
@Configuration(proxyBeanMethods = false)
@EnableWebMvc
static class WebConfiguration {
@Bean
CounterController counterController() {
return new CounterController(new AtomicInteger(40));
}
}
@RestController
private static class HelloController {
@GetMapping(path = "/hello", produces = "text/plain")
public String hello() {
return "Hello World";
}
@GetMapping("/error")
public String error() {
throw new IllegalStateException("Expected");
}
@GetMapping(path = "/json", produces = "application/json")
public String json() {
return """
{
"message": "Hello World",
"counter": 42
}""";
}
}
private record Message(String message, int counter) {}
@RestController
private static class CounterController {
private final AtomicInteger counter;
public CounterController(AtomicInteger counter) {
this.counter = counter;
}
public CounterController() {
this(new AtomicInteger());
}
@PostMapping("/increase")
public String increase() {
int value = this.counter.incrementAndGet();
return "counter " + value;
}
}
}

182
spring-test/src/test/java/org/springframework/test/web/servlet/assertj/CookieMapAssertTests.java

@ -0,0 +1,182 @@ @@ -0,0 +1,182 @@
/*
* 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.time.Duration;
import java.util.List;
import jakarta.servlet.http.Cookie;
import org.assertj.core.api.AssertProvider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link CookieMapAssert}.
*
* @author Brian Clozel
*/
class CookieMapAssertTests {
static Cookie[] cookies;
@BeforeAll
static void setup() {
Cookie framework = new Cookie("framework", "spring");
framework.setSecure(true);
framework.setHttpOnly(true);
Cookie age = new Cookie("age", "value");
age.setMaxAge(1200);
Cookie domain = new Cookie("domain", "value");
domain.setDomain("spring.io");
Cookie path = new Cookie("path", "value");
path.setPath("/spring");
cookies = List.of(framework, age, domain, path).toArray(new Cookie[0]);
}
@Test
void containsCookieWhenCookieExistsShouldPass() {
assertThat(forCookies()).containsCookie("framework");
}
@Test
void containsCookieWhenCookieMissingShouldFail() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertThat(forCookies()).containsCookie("missing"));
}
@Test
void containsCookiesWhenCookiesExistShouldPass() {
assertThat(forCookies()).containsCookies("framework", "age");
}
@Test
void containsCookiesWhenCookieMissingShouldFail() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertThat(forCookies()).containsCookies("framework", "missing"));
}
@Test
void doesNotContainCookieWhenCookieMissingShouldPass() {
assertThat(forCookies()).doesNotContainCookie("missing");
}
@Test
void doesNotContainCookieWhenCookieExistsShouldFail() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertThat(forCookies()).doesNotContainCookie("framework"));
}
@Test
void doesNotContainCookiesWhenCookiesMissingShouldPass() {
assertThat(forCookies()).doesNotContainCookies("missing", "missing2");
}
@Test
void doesNotContainCookiesWhenAtLeastOneCookieExistShouldFail() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertThat(forCookies()).doesNotContainCookies("missing", "framework"));
}
@Test
void hasValueEqualsWhenCookieValueMatchesShouldPass() {
assertThat(forCookies()).hasValue("framework", "spring");
}
@Test
void hasValueEqualsWhenCookieValueDiffersShouldFail() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertThat(forCookies()).hasValue("framework", "other"));
}
@Test
void hasCookieSatisfyingWhenCookieValueMatchesShouldPass() {
assertThat(forCookies()).hasCookieSatisfying("framework", cookie ->
assertThat(cookie.getValue()).startsWith("spr"));
}
@Test
void hasCookieSatisfyingWhenCookieValueDiffersShouldFail() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertThat(forCookies()).hasCookieSatisfying("framework", cookie ->
assertThat(cookie.getValue()).startsWith("not")));
}
@Test
void hasMaxAgeWhenCookieAgeMatchesShouldPass() {
assertThat(forCookies()).hasMaxAge("age", Duration.ofMinutes(20));
}
@Test
void hasMaxAgeWhenCookieAgeDiffersShouldFail() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertThat(forCookies()).hasMaxAge("age", Duration.ofMinutes(30)));
}
@Test
void pathWhenCookiePathMatchesShouldPass() {
assertThat(forCookies()).hasPath("path", "/spring");
}
@Test
void pathWhenCookiePathDiffersShouldFail() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertThat(forCookies()).hasPath("path", "/other"));
}
@Test
void hasDomainWhenCookieDomainMatchesShouldPass() {
assertThat(forCookies()).hasDomain("domain", "spring.io");
}
@Test
void hasDomainWhenCookieDomainDiffersShouldFail() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertThat(forCookies()).hasDomain("domain", "example.org"));
}
@Test
void isSecureWhenCookieSecureMatchesShouldPass() {
assertThat(forCookies()).isSecure("framework", true);
}
@Test
void isSecureWhenCookieSecureDiffersShouldFail() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertThat(forCookies()).isSecure("domain", true));
}
@Test
void isHttpOnlyWhenCookieHttpOnlyMatchesShouldPass() {
assertThat(forCookies()).isHttpOnly("framework", true);
}
@Test
void isHttpOnlyWhenCookieHttpOnlyDiffersShouldFail() {
assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
assertThat(forCookies()).isHttpOnly("domain", true));
}
private AssertProvider<CookieMapAssert> forCookies() {
return () -> new CookieMapAssert(cookies);
}
}

107
spring-test/src/test/java/org/springframework/test/web/servlet/assertj/DefaultAssertableMvcResultTests.java

@ -0,0 +1,107 @@ @@ -0,0 +1,107 @@
/*
* 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.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.web.servlet.MvcResult;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link DefaultAssertableMvcResult}.
*
* @author Stephane Nicoll
*/
class DefaultAssertableMvcResultTests {
@Test
void createWithMvcResultDelegatesToIt() {
MockHttpServletRequest request = new MockHttpServletRequest();
MvcResult mvcResult = mock(MvcResult.class);
given(mvcResult.getRequest()).willReturn(request);
DefaultAssertableMvcResult result = new DefaultAssertableMvcResult(mvcResult, null, null);
assertThat(result.getRequest()).isSameAs(request);
verify(mvcResult).getRequest();
}
@Test
void createWithExceptionDoesNotAllowAccessToRequest() {
assertRequestHasFailed(DefaultAssertableMvcResult::getRequest);
}
@Test
void createWithExceptionDoesNotAllowAccessToResponse() {
assertRequestHasFailed(DefaultAssertableMvcResult::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));
}
@Test
void createWithExceptionReturnsException() {
IllegalStateException exception = new IllegalStateException("Expected");
DefaultAssertableMvcResult result = new DefaultAssertableMvcResult(null, exception, null);
assertThat(result.getUnresolvedException()).isSameAs(exception);
}
private void assertRequestHasFailed(Consumer<DefaultAssertableMvcResult> action) {
DefaultAssertableMvcResult result = new DefaultAssertableMvcResult(null, new IllegalStateException("Expected"), null);
assertThatIllegalStateException().isThrownBy(() -> action.accept(result))
.withMessageContaining("Request has failed with unresolved exception");
}
}

142
spring-test/src/test/java/org/springframework/test/web/servlet/assertj/HandlerResultAssertTests.java

@ -0,0 +1,142 @@ @@ -0,0 +1,142 @@
/*
* 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.lang.reflect.Method;
import org.assertj.core.api.AssertProvider;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link HandlerResultAssert}.
*
* @author Stephane Nicoll
*/
class HandlerResultAssertTests {
@Test
void hasTypeUseController() {
assertThat(handlerMethod(new TestController(), "greet")).hasType(TestController.class);
}
@Test
void isMethodHandlerWithMethodHandler() {
assertThat(handlerMethod(new TestController(), "greet")).isMethodHandler();
}
@Test
void isMethodHandlerWithServletHandler() {
AssertProvider<HandlerResultAssert> actual = handler(new DefaultServletHttpRequestHandler());
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(actual).isMethodHandler())
.withMessageContainingAll(DefaultServletHttpRequestHandler.class.getName(),
HandlerMethod.class.getName());
}
@Test
void methodName() {
assertThat(handlerMethod(new TestController(), "greet")).method().hasName("greet");
}
@Test
void declaringClass() {
assertThat(handlerMethod(new TestController(), "greet")).method().hasDeclaringClass(TestController.class);
}
@Test
void method() {
assertThat(handlerMethod(new TestController(), "greet")).method().isEqualTo(
ReflectionUtils.findMethod(TestController.class, "greet"));
}
@Test
void methodWithServletHandler() {
AssertProvider<HandlerResultAssert> actual = handler(new DefaultServletHttpRequestHandler());
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(actual).method())
.withMessageContainingAll(DefaultServletHttpRequestHandler.class.getName(),
HandlerMethod.class.getName());
}
@Test
void isInvokedOn() {
assertThat(handlerMethod(new TestController(), "greet"))
.isInvokedOn(TestController.class, TestController::greet);
}
@Test
void isInvokedOnWithVoidMethod() {
assertThat(handlerMethod(new TestController(), "update"))
.isInvokedOn(TestController.class, controller -> {
controller.update();
return controller;
});
}
@Test
void isInvokedOnWithWrongMethod() {
AssertProvider<HandlerResultAssert> actual = handlerMethod(new TestController(), "update");
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(actual).isInvokedOn(TestController.class, TestController::greet))
.withMessageContainingAll(
method(TestController.class, "greet").toGenericString(),
method(TestController.class, "update").toGenericString());
}
private static AssertProvider<HandlerResultAssert> handler(Object instance) {
return () -> new HandlerResultAssert(instance);
}
private static AssertProvider<HandlerResultAssert> handlerMethod(Object instance, String name, Class<?>... parameterTypes) {
HandlerMethod handlerMethod = new HandlerMethod(instance, method(instance.getClass(), name, parameterTypes));
return () -> new HandlerResultAssert(handlerMethod);
}
private static Method method(Class<?> target, String name, Class<?>... parameterTypes) {
Method method = ReflectionUtils.findMethod(target, name, parameterTypes);
Assertions.assertThat(method).isNotNull();
return method;
}
@RestController
public static class TestController {
@GetMapping("/greet")
public ResponseEntity<String> greet() {
return ResponseEntity.ok().body("Hello");
}
@PostMapping("/update")
public void update() {
}
}
}

176
spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ModelAssertTests.java

@ -0,0 +1,176 @@ @@ -0,0 +1,176 @@
/*
* 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.util.HashMap;
import java.util.Map;
import org.assertj.core.api.AssertProvider;
import org.junit.jupiter.api.Test;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.validation.BindException;
import org.springframework.validation.DataBinder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link ModelAssert}.
*
* @author Stephane Nicoll
*/
class ModelAssertTests {
@Test
void hasErrors() {
assertThat(forModel(new TestBean(), Map.of("name", "John", "age", "4x"))).hasErrors();
}
@Test
void hasErrorsWithNoError() {
AssertProvider<ModelAssert> actual = forModel(new TestBean(), Map.of("name", "John", "age", "42"));
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(actual).hasErrors())
.withMessageContainingAll("John", "to have at least one error");
}
@Test
void doesNotHaveErrors() {
assertThat(forModel(new TestBean(), Map.of("name", "John", "age", "42"))).doesNotHaveErrors();
}
@Test
void doesNotHaveErrorsWithError() {
AssertProvider<ModelAssert> actual = forModel(new TestBean(), Map.of("name", "John", "age", "4x"));
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(actual).doesNotHaveErrors())
.withMessageContainingAll("John", "to not have an error, but got 1");
}
@Test
void extractBindingResultForAttributeInError() {
Map<String, Object> model = new HashMap<>();
augmentModel(model, "person", new TestBean(), Map.of("name", "John", "age", "4x", "touchy", "invalid.value"));
assertThat(forModel(model)).extractingBindingResult("person").hasErrorsCount(2);
}
@Test
void hasErrorCountForUnknownAttribute() {
Map<String, Object> model = new HashMap<>();
augmentModel(model, "person", new TestBean(), Map.of("name", "John", "age", "42"));
AssertProvider<ModelAssert> actual = forModel(model);
assertThatExceptionOfType(AssertionError.class).isThrownBy(
() -> assertThat(actual).extractingBindingResult("user"))
.withMessageContainingAll("to have a binding result for attribute 'user'");
}
@Test
void hasErrorsWithMatchingAttributes() {
Map<String, Object> model = new HashMap<>();
augmentModel(model, "wrong1", new TestBean(), Map.of("name", "first", "age", "4x"));
augmentModel(model, "valid", new TestBean(), Map.of("name", "second"));
augmentModel(model, "wrong2", new TestBean(), Map.of("name", "third", "touchy", "invalid.name"));
assertThat(forModel(model)).hasAttributeErrors("wrong1", "wrong2");
}
@Test
void hasErrorsWithOneNonMatchingAttribute() {
Map<String, Object> model = new HashMap<>();
augmentModel(model, "wrong1", new TestBean(), Map.of("name", "first", "age", "4x"));
augmentModel(model, "valid", new TestBean(), Map.of("name", "second"));
augmentModel(model, "wrong2", new TestBean(), Map.of("name", "third", "touchy", "invalid.name"));
AssertProvider<ModelAssert> actual = forModel(model);
assertThatExceptionOfType(AssertionError.class).isThrownBy(
() -> assertThat(actual).hasAttributeErrors("wrong1", "valid"))
.withMessageContainingAll("to have attribute errors for:", "wrong1, valid",
"but these attributes do not have any error:", "valid");
}
@Test
void hasErrorsWithOneNonMatchingAttributeAndOneUnknownAttribute() {
Map<String, Object> model = new HashMap<>();
augmentModel(model, "wrong1", new TestBean(), Map.of("name", "first", "age", "4x"));
augmentModel(model, "valid", new TestBean(), Map.of("name", "second"));
augmentModel(model, "wrong2", new TestBean(), Map.of("name", "third", "touchy", "invalid.name"));
AssertProvider<ModelAssert> actual = forModel(model);
assertThatExceptionOfType(AssertionError.class).isThrownBy(
() -> assertThat(actual).hasAttributeErrors("wrong1", "unknown", "valid"))
.withMessageContainingAll("to have attribute errors for:", "wrong1, unknown, valid",
"but could not find these attributes:", "unknown",
"and these attributes do not have any error:", "valid");
}
@Test
void doesNotHaveErrorsWithMatchingAttributes() {
Map<String, Object> model = new HashMap<>();
augmentModel(model, "valid1", new TestBean(), Map.of("name", "first"));
augmentModel(model, "wrong", new TestBean(), Map.of("name", "second", "age", "4x"));
augmentModel(model, "valid2", new TestBean(), Map.of("name", "third"));
assertThat(forModel(model)).doesNotHaveAttributeErrors("valid1", "valid2");
}
@Test
void doesNotHaveErrorsWithOneNonMatchingAttribute() {
Map<String, Object> model = new HashMap<>();
augmentModel(model, "valid1", new TestBean(), Map.of("name", "first"));
augmentModel(model, "wrong", new TestBean(), Map.of("name", "second", "age", "4x"));
augmentModel(model, "valid2", new TestBean(), Map.of("name", "third"));
AssertProvider<ModelAssert> actual = forModel(model);
assertThatExceptionOfType(AssertionError.class).isThrownBy(
() -> assertThat(actual).doesNotHaveAttributeErrors("valid1", "wrong"))
.withMessageContainingAll("to have attribute without errors for:", "valid1, wrong",
"but these attributes have at least an error:", "wrong");
}
@Test
void doesNotHaveErrorsWithOneNonMatchingAttributeAndOneUnknownAttribute() {
Map<String, Object> model = new HashMap<>();
augmentModel(model, "valid1", new TestBean(), Map.of("name", "first"));
augmentModel(model, "wrong", new TestBean(), Map.of("name", "second", "age", "4x"));
augmentModel(model, "valid2", new TestBean(), Map.of("name", "third"));
AssertProvider<ModelAssert> actual = forModel(model);
assertThatExceptionOfType(AssertionError.class).isThrownBy(
() -> assertThat(actual).doesNotHaveAttributeErrors("valid1", "unknown", "wrong"))
.withMessageContainingAll("to have attribute without errors for:", "valid1, unknown, wrong",
"but could not find these attributes:", "unknown",
"and these attributes have at least an error:", "wrong");
}
private AssertProvider<ModelAssert> forModel(Map<String, Object> model) {
return () -> new ModelAssert(model);
}
private AssertProvider<ModelAssert> forModel(Object instance, Map<String, Object> propertyValues) {
Map<String, Object> model = new HashMap<>();
augmentModel(model, "test", instance, propertyValues);
return forModel(model);
}
private static void augmentModel(Map<String, Object> model, String attribute, Object instance, Map<String, Object> propertyValues) {
DataBinder binder = new DataBinder(instance, attribute);
MutablePropertyValues pvs = new MutablePropertyValues(propertyValues);
binder.bind(pvs);
try {
binder.close();
model.putAll(binder.getBindingResult().getModel());
}
catch (BindException ex) {
model.putAll(ex.getBindingResult().getModel());
}
}
}

88
spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java

@ -0,0 +1,88 @@ @@ -0,0 +1,88 @@
/*
* 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<ResponseBodyAssert> fromResponse(MockHttpServletResponse response) {
return () -> new ResponseBodyAssert(response.getContentAsByteArray(), Charset.forName(response.getCharacterEncoding()), null);
}
}

6
spring-test/src/test/resources/org/springframework/test/json/different.json

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
{
"gnirps": [
"boot",
"framework"
]
}

4
spring-test/src/test/resources/org/springframework/test/json/example.json

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
{
"name": "Spring",
"age": 123
}

6
spring-test/src/test/resources/org/springframework/test/json/lenient-same.json

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
{
"spring": [
"framework",
"boot"
]
}

4
spring-test/src/test/resources/org/springframework/test/json/nulls.json

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
{
"valuename": "spring",
"nullname": null
}

36
spring-test/src/test/resources/org/springframework/test/json/simpsons.json

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
{
"familyMembers": [
{
"name": "Homer"
},
{
"name": "Marge"
},
{
"name": "Bart"
},
{
"name": "Lisa"
},
{
"name": "Maggie"
}
],
"indexedFamilyMembers": {
"father": {
"name": "Homer"
},
"mother": {
"name": "Marge"
},
"son": {
"name": "Bart"
},
"daughter": {
"name": "Lisa"
},
"baby": {
"name": "Maggie"
}
}
}

6
spring-test/src/test/resources/org/springframework/test/json/source.json

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
{
"spring": [
"boot",
"framework"
]
}

18
spring-test/src/test/resources/org/springframework/test/json/types.json

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
{
"str": "foo",
"num": 5,
"pi": 3.1415926,
"bool": true,
"arr": [
42
],
"colorMap": {
"red": "rojo"
},
"whitespace": " ",
"emptyString": "",
"emptyArray": [
],
"emptyMap": {
}
}

3
spring-test/src/test/resources/org/springframework/test/web/servlet/assertj/message.json

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
{
"message": "hello"
}
Loading…
Cancel
Save