Move response body directly in AbstractMockHttpServletResponseAssert

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

This avoids a double navigation to assert the response body.

See gh-32712
This commit is contained in:
Stéphane Nicoll
2024-05-06 15:11:30 +02:00
parent 24cc77655f
commit 5567d14700
10 changed files with 195 additions and 301 deletions

View File

@@ -52,7 +52,10 @@ import org.springframework.util.function.ThrowingBiFunction;
* assertions} on the value.
*
* <p>Also support comparing the JSON document against a target, using
* {@linkplain JSONCompare JSON Assert}.
* {@linkplain JSONCompare JSON Assert}. Resources that are loaded from
* the classpath can be relative if a {@linkplain #withResourceLoadClass(Class)
* class} is provided. By default, {@code UTF-8} is used to load resources
* but this can be overridden using {@link #withCharset(Charset)}.
*
* @author Stephane Nicoll
* @author Phillip Webb
@@ -71,28 +74,27 @@ public abstract class AbstractJsonContentAssert<SELF extends AbstractJsonContent
@Nullable
private final GenericHttpMessageConverter<Object> jsonMessageConverter;
private final JsonLoader jsonLoader;
@Nullable
private Class<?> resourceLoadClass;
@Nullable
private Charset charset;
private JsonLoader jsonLoader;
/**
* Create an assert for the given JSON document.
* <p>Path can be converted to a value object using the given
* {@linkplain GenericHttpMessageConverter json message converter}.
* <p>Resources to match can be loaded relative to the given
* {@code resourceLoadClass}. If not specified, resources must always be
* absolute. A specific {@link Charset} can be provided if {@code UTF-8} is
* not suitable.
* @param json the JSON document to assert
* @param jsonMessageConverter the converter to use
* @param resourceLoadClass the class used to load resources
* @param charset the charset of the JSON resources
* @param selfType the implementation type of this assert
*/
protected AbstractJsonContentAssert(@Nullable String json,
@Nullable GenericHttpMessageConverter<Object> jsonMessageConverter, @Nullable Class<?> resourceLoadClass,
@Nullable Charset charset, Class<?> selfType) {
@Nullable GenericHttpMessageConverter<Object> jsonMessageConverter, Class<?> selfType) {
super(json, selfType);
this.jsonMessageConverter = jsonMessageConverter;
this.jsonLoader = new JsonLoader(resourceLoadClass, charset);
this.jsonLoader = new JsonLoader(null, null);
as("JSON content");
}
@@ -376,6 +378,31 @@ public abstract class AbstractJsonContentAssert<SELF extends AbstractJsonContent
return isNotEqualTo(expected, JSONCompareMode.STRICT);
}
/**
* Override the class used to load resources. Resources can be loaded from
* an absolute location or relative to tpe specified class. For instance,
* specifying {@code com.example.MyClass} as the resource class allows you
* to use "my-file.json" to load {@code /com/example/my-file.json}.
* @param resourceLoadClass the class used to load resources or {@code null}
* to only use absolute paths.
*/
public SELF withResourceLoadClass(@Nullable Class<?> resourceLoadClass) {
this.resourceLoadClass = resourceLoadClass;
this.jsonLoader = new JsonLoader(resourceLoadClass, this.charset);
return this.myself;
}
/**
* Override the {@link Charset} to use to load resources. By default,
* resources are loaded using {@code UTF-8}.
* @param charset the charset to use, or {@code null} to use the default
*/
public SELF withCharset(@Nullable Charset charset) {
this.charset = charset;
this.jsonLoader = new JsonLoader(this.resourceLoadClass, charset);
return this.myself;
}
private JSONCompareResult compare(@Nullable CharSequence expectedJson, JSONCompareMode compareMode) {
return compare(this.actual, expectedJson, (actualJsonString, expectedJsonString) ->

View File

@@ -55,7 +55,7 @@ public final class JsonContent implements AssertProvider<JsonContentAssert> {
*/
@Override
public JsonContentAssert assertThat() {
return new JsonContentAssert(this.json, null, this.resourceLoadClass, null);
return new JsonContentAssert(this.json, null).withResourceLoadClass(this.resourceLoadClass);
}
/**

View File

@@ -16,8 +16,6 @@
package org.springframework.test.json;
import java.nio.charset.Charset;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.lang.Nullable;
@@ -33,19 +31,11 @@ public class JsonContentAssert extends AbstractJsonContentAssert<JsonContentAsse
* Create an assert for the given JSON document.
* <p>Path can be converted to a value object using the given
* {@linkplain GenericHttpMessageConverter json message converter}.
* <p>Resources to match can be loaded relative to the given
* {@code resourceLoadClass}. If not specified, resources must always be
* absolute. A specific {@link Charset} can be provided if {@code UTF-8} is
* not suitable.
* @param json the JSON document to assert
* @param jsonMessageConverter the converter to use
* @param resourceLoadClass the class used to load resources
* @param charset the charset of the JSON resources
*/
public JsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter,
@Nullable Class<?> resourceLoadClass, @Nullable Charset charset) {
super(json, jsonMessageConverter, resourceLoadClass, charset, JsonContentAssert.class);
public JsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter<Object> jsonMessageConverter) {
super(json, jsonMessageConverter, JsonContentAssert.class);
}
}

View File

@@ -18,9 +18,16 @@ package org.springframework.test.web.servlet.assertj;
import java.nio.charset.Charset;
import org.assertj.core.api.AbstractByteArrayAssert;
import org.assertj.core.api.AbstractStringAssert;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.ByteArrayAssert;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.json.AbstractJsonContentAssert;
import org.springframework.test.json.JsonContentAssert;
import org.springframework.test.web.UriAssert;
/**
@@ -45,22 +52,62 @@ public abstract class AbstractMockHttpServletResponseAssert<SELF extends Abstrac
this.jsonMessageConverter = jsonMessageConverter;
}
/**
* Return a new {@linkplain ResponseBodyAssert assertion} object that uses
* the response body as the object to test. The returned assertion object
* provides access to the raw byte array, a String value decoded using the
* response's character encoding, and dedicated JSON testing support.
* Return a new {@linkplain AbstractStringAssert assertion} object that uses
* the response body converted to text as the object to test.
* <p>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");
* assertThat(response).bodyText().isEqualTo("Hello World");
* </code></pre>
*/
public ResponseBodyAssert body() {
return new ResponseBodyAssert(getResponse().getContentAsByteArray(),
Charset.forName(getResponse().getCharacterEncoding()), this.jsonMessageConverter);
public AbstractStringAssert<?> bodyText() {
return Assertions.assertThat(readBody());
}
/**
* Return a new {@linkplain AbstractJsonContentAssert assertion} object that
* uses the response body converted to text as the object to test. Compared
* to {@link #bodyText()}, the assertion object provides dedicated JSON
* support.
* <p>Examples: <pre><code class='java'>
* // Check that the response body is strictly equal to the content of
* // "/com/acme/sample/person-created.json":
* assertThat(response).bodyJson()
* .isStrictlyEqualToJson("/com/acme/sample/person-created.json");
*
* // Check that the response is strictly equal to the content of the
* // specified file located in the same package as the PersonController:
* assertThat(response).bodyJson().withResourceLoadClass(PersonController.class)
* .isStrictlyEqualToJson("person-created.json");
* </code></pre>
* The returned assert object also supports JSON path expressions.
* <p>Examples: <pre><code class='java'>
* // Check that the JSON document does not have an "error" element
* assertThat(response).bodyJson().doesNotHavePath("$.error");
*
* // Check that the JSON document as a top level "message" element
* assertThat(response).bodyJson()
* .extractingPath("$.message").asString().isEqualTo("hello");
* </code></pre>
*/
public AbstractJsonContentAssert<?> bodyJson() {
return new JsonContentAssert(readBody(), this.jsonMessageConverter);
}
private String readBody() {
return new String(getResponse().getContentAsByteArray(),
Charset.forName(getResponse().getCharacterEncoding()));
}
/**
* Return a new {@linkplain AbstractByteArrayAssert assertion} object that
* uses the response body as the object to test.
* @see #bodyText()
* @see #bodyJson()
*/
public AbstractByteArrayAssert<?> body() {
return new ByteArrayAssert(getResponse().getContentAsByteArray());
}
/**
@@ -89,6 +136,14 @@ public abstract class AbstractMockHttpServletResponseAssert<SELF extends Abstrac
return new UriAssert(getResponse().getRedirectedUrl(), "Redirected URL");
}
/**
* Verify that the response body is equal to the given value.
*/
public SELF hasBodyTextEqualTo(String bodyText) {
bodyText().isEqualTo(bodyText);
return this.myself;
}
/**
* Verify that the forwarded URL is equal to the given value.
* @param forwardedUrl the expected forwarded URL (can be null)

View File

@@ -1,130 +0,0 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.servlet.assertj;
import java.nio.charset.Charset;
import jakarta.servlet.http.HttpServletResponse;
import org.assertj.core.api.AbstractByteArrayAssert;
import org.assertj.core.api.AbstractStringAssert;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.test.json.AbstractJsonContentAssert;
import org.springframework.test.json.JsonContentAssert;
/**
* 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 AbstractJsonContentAssert assertion} object that
* provides {@linkplain com.jayway.jsonpath.JsonPath JSON path} assertions on
* the response body.
*/
public AbstractJsonContentAssert<?> jsonPath() {
return new JsonContentAssert(getJson(), this.jsonMessageConverter, null, this.characterEncoding)
.as("JSON body");
}
/**
* Return a new {@linkplain AbstractJsonContentAssert assertion} object that
* provides support for {@linkplain org.skyscreamer.jsonassert.JSONCompareMode
* JSON assert} comparisons against expected JSON input which can be loaded
* from the classpath.
* <p>This method only supports absolute locations for JSON documents loaded
* from the classpath. Consider using {@link #json(Class)} to load JSON
* documents relative to a given class.
* <p>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 AbstractJsonContentAssert<?> json() {
return json(null);
}
/**
* Return a new {@linkplain AbstractJsonContentAssert assertion} object that
* provides support for {@linkplain org.skyscreamer.jsonassert.JSONCompareMode
* JSON assert} comparisons against expected JSON input which can be loaded
* from the classpath.
* <p>Locations for JSON documents can be absolute using a leading slash, or
* relative to the given {@code resourceLoadClass}.
* <p>Example: <pre><code class='java'>
* // Check that the response is strictly equal to the content of the
* // specified file located in the same package as the PersonController:
* assertThat(...).body().json(PersonController.class)
* .isStrictlyEqualToJson("person-created.json");
* </code></pre>
* @param resourceLoadClass the class used to load relative JSON documents
* @see ClassPathResource#ClassPathResource(String, Class)
*/
public AbstractJsonContentAssert<?> json(@Nullable Class<?> resourceLoadClass) {
return new JsonContentAssert(getJson(), this.jsonMessageConverter, resourceLoadClass, this.characterEncoding)
.as("JSON body");
}
/**
* Verify that the response body is equal to the given {@link String}.
* <p>Converts the actual byte array to a String using the character encoding
* of the {@link HttpServletResponse}.
* @param expected the expected content of the response body
* @see #asString()
*/
public ResponseBodyAssert isEqualTo(String expected) {
asString().isEqualTo(expected);
return this;
}
/**
* Override that uses the character encoding of the {@link HttpServletResponse}
* to convert the byte[] to a String, rather than the platform's default charset.
*/
@Override
public AbstractStringAssert<?> asString() {
return asString(this.characterEncoding);
}
private String getJson() {
return new String(this.actual, this.characterEncoding);
}
}