Browse Source

AssertJ support for WebTestClient

Closes gh-35737
pull/35768/head
rstoyanchev 1 month ago
parent
commit
7445f542f4
  1. 3
      spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java
  2. 17
      spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java
  3. 45
      spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java
  4. 9
      spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java
  5. 51
      spring-test/src/main/java/org/springframework/test/web/reactive/server/assertj/DefaultWebTestClientResponse.java
  6. 163
      spring-test/src/main/java/org/springframework/test/web/reactive/server/assertj/ResponseCookieMapAssert.java
  7. 64
      spring-test/src/main/java/org/springframework/test/web/reactive/server/assertj/WebTestClientResponse.java
  8. 337
      spring-test/src/main/java/org/springframework/test/web/reactive/server/assertj/WebTestClientResponseAssert.java
  9. 7
      spring-test/src/main/java/org/springframework/test/web/reactive/server/assertj/package-info.java
  10. 2
      spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java
  11. 176
      spring-test/src/test/java/org/springframework/test/web/reactive/server/assertj/ResponseCookieMapAssertTests.java
  12. 126
      spring-test/src/test/java/org/springframework/test/web/reactive/server/assertj/WebTestClientResponseTests.java

3
spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java

@ -111,9 +111,10 @@ class DefaultWebTestClient implements WebTestClient { @@ -111,9 +111,10 @@ class DefaultWebTestClient implements WebTestClient {
Consumer<EntityExchangeResult<?>> entityResultConsumer,
@Nullable Duration responseTimeout, DefaultWebTestClientBuilder clientBuilder) {
this.wiretapConnector = new WiretapConnector(connector);
this.jsonEncoderDecoder = JsonEncoderDecoder.from(
exchangeStrategies.messageWriters(), exchangeStrategies.messageReaders());
this.wiretapConnector = new WiretapConnector(connector, this.jsonEncoderDecoder);
this.exchangeFunction = exchangeFactory.apply(this.wiretapConnector);
this.uriBuilderFactory = uriBuilderFactory;
this.defaultHeaders = headers;

17
spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java

@ -36,6 +36,7 @@ import org.springframework.http.MediaType; @@ -36,6 +36,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.test.json.JsonConverterDelegate;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
@ -78,6 +79,8 @@ public class ExchangeResult { @@ -78,6 +79,8 @@ public class ExchangeResult {
private final @Nullable Object mockServerResult;
private final @Nullable JsonConverterDelegate converterDelegate;
/** Ensure single logging, for example, for expectAll. */
private boolean diagnosticsLogged;
@ -93,10 +96,11 @@ public class ExchangeResult { @@ -93,10 +96,11 @@ public class ExchangeResult {
* @param timeout how long to wait for content to materialize
* @param uriTemplate the URI template used to set up the request, if any
* @param serverResult the result of a mock server exchange if applicable.
* @param converterDelegate for JSON decoding in AssertJ support
*/
ExchangeResult(ClientHttpRequest request, ClientHttpResponse response,
Mono<byte[]> requestBody, Mono<byte[]> responseBody, Duration timeout, @Nullable String uriTemplate,
@Nullable Object serverResult) {
@Nullable Object serverResult, @Nullable JsonConverterDelegate converterDelegate) {
Assert.notNull(request, "ClientHttpRequest is required");
Assert.notNull(response, "ClientHttpResponse is required");
@ -110,6 +114,7 @@ public class ExchangeResult { @@ -110,6 +114,7 @@ public class ExchangeResult {
this.timeout = timeout;
this.uriTemplate = uriTemplate;
this.mockServerResult = serverResult;
this.converterDelegate = converterDelegate;
}
/**
@ -123,6 +128,7 @@ public class ExchangeResult { @@ -123,6 +128,7 @@ public class ExchangeResult {
this.timeout = other.timeout;
this.uriTemplate = other.uriTemplate;
this.mockServerResult = other.mockServerResult;
this.converterDelegate = other.converterDelegate;
this.diagnosticsLogged = other.diagnosticsLogged;
}
@ -206,6 +212,15 @@ public class ExchangeResult { @@ -206,6 +212,15 @@ public class ExchangeResult {
return this.mockServerResult;
}
/**
* Return a {@link JsonConverterDelegate} based on the configured codecs.
* Mainly for internal use from AssertJ support classes.
* @since 7.0
*/
public @Nullable JsonConverterDelegate getJsonConverterDelegate() {
return this.converterDelegate;
}
/**
* Execute the given Runnable, catch any {@link AssertionError}, log details
* about the request and response at ERROR level under the class log

45
spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
package org.springframework.test.web.reactive.server;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Map;
import java.util.stream.Stream;
@ -25,11 +26,17 @@ import org.jspecify.annotations.Nullable; @@ -25,11 +26,17 @@ import org.jspecify.annotations.Nullable;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.DecoderHttpMessageReader;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.test.json.JsonConverterDelegate;
import org.springframework.util.Assert;
import org.springframework.util.MimeTypeUtils;
/**
* {@link Encoder} and {@link Decoder} that is able to encode and decode
@ -49,6 +56,14 @@ record JsonEncoderDecoder(Encoder<?> encoder, Decoder<?> decoder) { @@ -49,6 +56,14 @@ record JsonEncoderDecoder(Encoder<?> encoder, Decoder<?> decoder) {
private static final ResolvableType MAP_TYPE = ResolvableType.forClass(Map.class);
/**
* Return a {@link JsonConverterDelegate} that uses the encoder and decoder.
*/
public JsonConverterDelegate createJsonConverterDelegate() {
return new CodecsJsonConverterDelegate();
}
/**
* Create a {@link JsonEncoderDecoder} instance based on the specified
* infrastructure.
@ -107,4 +122,34 @@ record JsonEncoderDecoder(Encoder<?> encoder, Decoder<?> decoder) { @@ -107,4 +122,34 @@ record JsonEncoderDecoder(Encoder<?> encoder, Decoder<?> decoder) {
.orElse(null);
}
/**
* Implementation that delegates to the contained Encoder and Decoder.
*/
private class CodecsJsonConverterDelegate implements JsonConverterDelegate {
@SuppressWarnings("unchecked")
@Override
public <T> T read(String content, ResolvableType targetType) {
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = DefaultDataBufferFactory.sharedInstance.wrap(bytes);
Object value = decoder().decode(buffer, targetType, MediaType.APPLICATION_JSON, null);
Assert.state(value != null, () -> "Could not decode JSON content: " + content);
return (T) value;
}
@SuppressWarnings("unchecked")
@Override
public <T> T map(Object value, ResolvableType targetType) {
DataBuffer buffer = ((Encoder<T>) encoder()).encodeValue((T) value,
DefaultDataBufferFactory.sharedInstance, targetType, MimeTypeUtils.APPLICATION_JSON, null);
try {
return read(buffer.toString(StandardCharsets.UTF_8), targetType);
}
finally {
DataBufferUtils.release(buffer);
}
}
}
}

9
spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java

@ -38,6 +38,7 @@ import org.springframework.http.client.reactive.ClientHttpRequest; @@ -38,6 +38,7 @@ import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.client.reactive.ClientHttpRequestDecorator;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.client.reactive.ClientHttpResponseDecorator;
import org.springframework.test.json.JsonConverterDelegate;
import org.springframework.util.Assert;
/**
@ -53,11 +54,14 @@ class WiretapConnector implements ClientHttpConnector { @@ -53,11 +54,14 @@ class WiretapConnector implements ClientHttpConnector {
private final ClientHttpConnector delegate;
private final @Nullable JsonConverterDelegate converterDelegate;
private final Map<String, ClientExchangeInfo> exchanges = new ConcurrentHashMap<>();
WiretapConnector(ClientHttpConnector delegate) {
WiretapConnector(ClientHttpConnector delegate, @Nullable JsonEncoderDecoder encoderDecoder) {
this.delegate = delegate;
this.converterDelegate = (encoderDecoder != null ? encoderDecoder.createJsonConverterDelegate() : null);
}
@ -96,7 +100,8 @@ class WiretapConnector implements ClientHttpConnector { @@ -96,7 +100,8 @@ class WiretapConnector implements ClientHttpConnector {
clientInfo.getRequest().getRecorder().getContent(),
clientInfo.getResponse().getRecorder().getContent(),
timeout, uriTemplate,
clientInfo.getResponse().getMockServerResult());
clientInfo.getResponse().getMockServerResult(),
this.converterDelegate);
}

51
spring-test/src/main/java/org/springframework/test/web/reactive/server/assertj/DefaultWebTestClientResponse.java

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
/*
* Copyright 2002-present 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.reactive.server.assertj;
import org.springframework.test.web.reactive.server.ExchangeResult;
/**
* Default implementation of {@link WebTestClientResponse}.
*
* @author Rossen Stoyanchev
* @since 7.0
*/
final class DefaultWebTestClientResponse implements WebTestClientResponse {
private final ExchangeResult exchangeResult;
DefaultWebTestClientResponse(ExchangeResult exchangeResult) {
this.exchangeResult = exchangeResult;
}
@Override
public ExchangeResult getExchangeResult() {
return this.exchangeResult;
}
/**
* Use AssertJ's {@link org.assertj.core.api.Assertions#assertThat assertThat} instead.
*/
@Override
public WebTestClientResponseAssert assertThat() {
return new WebTestClientResponseAssert(this);
}
}

163
spring-test/src/main/java/org/springframework/test/web/reactive/server/assertj/ResponseCookieMapAssert.java

@ -0,0 +1,163 @@ @@ -0,0 +1,163 @@
/*
* Copyright 2002-present 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.reactive.server.assertj;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
import org.assertj.core.api.AbstractMapAssert;
import org.assertj.core.api.Assertions;
import org.springframework.http.ResponseCookie;
/**
* AssertJ {@linkplain org.assertj.core.api.Assert assertions} that can be applied
* to {@link ResponseCookie cookies}.
*
* @author Rossen Stoyanchev
* @since 7.0
*/
public class ResponseCookieMapAssert
extends AbstractMapAssert<ResponseCookieMapAssert, Map<String, ResponseCookie>, String, ResponseCookie> {
public ResponseCookieMapAssert(ResponseCookie[] actual) {
super(toMap(actual), ResponseCookieMapAssert.class);
as("Cookies");
}
private static Map<String, ResponseCookie> toMap(ResponseCookie[] cookies) {
Map<String, ResponseCookie> map = new LinkedHashMap<>();
for (ResponseCookie 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 ResponseCookieMapAssert containsCookie(String name) {
return containsKey(name);
}
/**
* Verify that the actual cookies contain cookies with the given {@code names}.
* @param names the names of expected cookies
* @see #containsKeys
*/
public ResponseCookieMapAssert 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 ResponseCookieMapAssert doesNotContainCookie(String name) {
return doesNotContainKey(name);
}
/**
* Verify that the actual cookies do not contain any cookies with the given
* {@code names}.
* @param names the names of cookies that should not be present
* @see #doesNotContainKeys
*/
public ResponseCookieMapAssert doesNotContainCookies(String... names) {
return doesNotContainKeys(names);
}
/**
* Verify that the actual cookies contain a cookie with the given {@code name}
* that satisfies the given {@code cookieRequirements}.
* @param name the name of an expected cookie
* @param cookieRequirements the requirements for the cookie
*/
public ResponseCookieMapAssert hasCookieSatisfying(String name, Consumer<ResponseCookie> cookieRequirements) {
return hasEntrySatisfying(name, cookieRequirements);
}
/**
* Verify that the actual cookies contain a cookie with the given {@code name}
* whose {@linkplain ResponseCookie#getValue() value} is equal to the given one.
* @param name the name of the cookie
* @param expected the expected value of the cookie
*/
public ResponseCookieMapAssert 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 ResponseCookie#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 ResponseCookieMapAssert hasMaxAge(String name, Duration expected) {
return hasCookieSatisfying(name, cookie -> Assertions.assertThat(cookie.getMaxAge()).isEqualTo(expected));
}
/**
* Verify that the actual cookies contain a cookie with the given {@code name}
* whose {@linkplain ResponseCookie#getPath() path} is equal to the given one.
* @param name the name of the cookie
* @param expected the expected path of the cookie
*/
public ResponseCookieMapAssert 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 ResponseCookie#getDomain() domain} is equal to the given one.
* @param name the name of the cookie
* @param expected the expected domain of the cookie
*/
public ResponseCookieMapAssert 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 ResponseCookie#isSecure() secure flag} is equal to the give one.
* @param name the name of the cookie
* @param expected whether the cookie is secure
*/
public ResponseCookieMapAssert isSecure(String name, boolean expected) {
return hasCookieSatisfying(name, cookie -> Assertions.assertThat(cookie.isSecure()).isEqualTo(expected));
}
/**
* Verify that the actual cookies contain a cookie with the given {@code name}
* whose {@linkplain ResponseCookie#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 ResponseCookieMapAssert isHttpOnly(String name, boolean expected) {
return hasCookieSatisfying(name, cookie -> Assertions.assertThat(cookie.isHttpOnly()).isEqualTo(expected));
}
}

64
spring-test/src/main/java/org/springframework/test/web/reactive/server/assertj/WebTestClientResponse.java

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
/*
* Copyright 2002-present 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.reactive.server.assertj;
import org.assertj.core.api.AssertProvider;
import org.springframework.test.web.reactive.server.ExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* {@link AssertProvider} for {@link WebTestClientResponseAssert} that holds the
* result of an exchange performed through {@link WebTestClient}. Intended for
* further use with AssertJ. For example:
*
* <pre class="code">
* ResponseSpec spec = webTestClient.get().uri("/greeting").exchange();
*
* WebTestClientResponse response = WebTestClientResponse.from(spec);
* assertThat(response).hasStatusOk();
* assertThat(response).contentType().isCompatibleWith(MediaType.APPLICATION_JSON);
* assertThat(response).bodyJson().extractingPath("$.message").asString().isEqualTo("Hello World");
* </pre>
*
* @author Rossen Stoyanchev
* @since 7.0
*/
public interface WebTestClientResponse extends AssertProvider<WebTestClientResponseAssert> {
/**
* Return the underlying {@link ExchangeResult}.
*/
ExchangeResult getExchangeResult();
/**
* Create an instance from a {@link WebTestClient.ResponseSpec}.
*/
static WebTestClientResponse from(WebTestClient.ResponseSpec spec) {
return from(spec.returnResult(byte[].class));
}
/**
* Create an instance from an {@link ExchangeResult}.
*/
static WebTestClientResponse from(ExchangeResult result) {
return new DefaultWebTestClientResponse(result);
}
}

337
spring-test/src/main/java/org/springframework/test/web/reactive/server/assertj/WebTestClientResponseAssert.java

@ -0,0 +1,337 @@ @@ -0,0 +1,337 @@
/*
* Copyright 2002-present 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.reactive.server.assertj;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.function.Supplier;
import org.assertj.core.api.AbstractByteArrayAssert;
import org.assertj.core.api.AbstractIntegerAssert;
import org.assertj.core.api.AbstractObjectAssert;
import org.assertj.core.api.AbstractStringAssert;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.ByteArrayAssert;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.test.http.HttpHeadersAssert;
import org.springframework.test.http.MediaTypeAssert;
import org.springframework.test.json.AbstractJsonContentAssert;
import org.springframework.test.json.JsonContent;
import org.springframework.test.json.JsonContentAssert;
import org.springframework.test.web.reactive.server.ExchangeResult;
import org.springframework.util.function.SingletonSupplier;
/**
* AssertJ {@linkplain org.assertj.core.api.Assert assertions} for the result
* from a {@link org.springframework.test.web.reactive.server.WebTestClient}
* exchange.
*
* @author Rossen Stoyanchev
* @since 7.0
*/
@SuppressWarnings({"UnusedReturnValue", "unused"})
public class WebTestClientResponseAssert
extends AbstractObjectAssert<WebTestClientResponseAssert, WebTestClientResponse> {
private final Supplier<MediaTypeAssert> contentTypeAssertSupplier;
private final Supplier<HttpHeadersAssert> headersAssertSupplier;
private final Supplier<AbstractIntegerAssert<?>> statusAssert;
WebTestClientResponseAssert(WebTestClientResponse actual) {
super(actual, WebTestClientResponseAssert.class);
this.contentTypeAssertSupplier = SingletonSupplier.of(() ->
new MediaTypeAssert(getExchangeResult().getResponseHeaders().getContentType()));
this.headersAssertSupplier = SingletonSupplier.of(() ->
new HttpHeadersAssert(getExchangeResult().getResponseHeaders()));
this.statusAssert = SingletonSupplier.of(() ->
Assertions.assertThat(getExchangeResult().getStatus().value()).as("HTTP status code"));
}
/**
* Verify that the HTTP status is equal to the specified status code.
* @param status the expected HTTP status code
*/
public WebTestClientResponseAssert 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 WebTestClientResponseAssert hasStatus(HttpStatus status) {
return hasStatus(status.value());
}
/**
* Verify that the HTTP status is equal to {@link HttpStatus#OK}.
* @see #hasStatus(HttpStatus)
*/
public WebTestClientResponseAssert 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 WebTestClientResponseAssert hasStatus1xxInformational() {
return hasStatusSeries(HttpStatus.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 WebTestClientResponseAssert hasStatus2xxSuccessful() {
return hasStatusSeries(HttpStatus.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 WebTestClientResponseAssert hasStatus3xxRedirection() {
return hasStatusSeries(HttpStatus.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 WebTestClientResponseAssert hasStatus4xxClientError() {
return hasStatusSeries(HttpStatus.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 WebTestClientResponseAssert hasStatus5xxServerError() {
return hasStatusSeries(HttpStatus.Series.SERVER_ERROR);
}
private WebTestClientResponseAssert hasStatusSeries(HttpStatus.Series series) {
HttpStatusCode status = getExchangeResult().getStatus();
Assertions.assertThat(HttpStatus.Series.resolve(status.value())).as("HTTP status series").isEqualTo(series);
return this.myself;
}
private AbstractIntegerAssert<?> status() {
return this.statusAssert.get();
}
/**
* Return a new {@linkplain HttpHeadersAssert assertion} object that uses
* {@link HttpHeaders} as the object to test. The returned assertion object
* provides all the regular {@linkplain org.assertj.core.api.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 response contains a header with the given {@code name}.
* @param name the name of an expected HTTP header
*/
public WebTestClientResponseAssert containsHeader(String name) {
headers().containsHeader(name);
return this.myself;
}
/**
* Verify that the response does not contain a header with the given {@code name}.
* @param name the name of an HTTP header that should not be present
*/
public WebTestClientResponseAssert doesNotContainHeader(String name) {
headers().doesNotContainHeader(name);
return this.myself;
}
/**
* Verify that the response contains a header with the given {@code name}
* and primary {@code value}.
* @param name the name of an expected HTTP header
* @param value the expected value of the header
*/
public WebTestClientResponseAssert hasHeader(String name, String value) {
headers().hasValue(name, value);
return this.myself;
}
/**
* Return a new {@linkplain MediaTypeAssert assertion} object that uses the
* response's {@linkplain MediaType content type} as the object to test.
*/
public MediaTypeAssert contentType() {
return this.contentTypeAssertSupplier.get();
}
/**
* Verify that the response's {@code Content-Type} is equal to the given value.
* @param contentType the expected content type
*/
public WebTestClientResponseAssert hasContentType(MediaType contentType) {
contentType().isEqualTo(contentType);
return this.myself;
}
/**
* Verify that the response's {@code Content-Type} is equal to the given
* string representation.
* @param contentType the expected content type
*/
public WebTestClientResponseAssert hasContentType(String contentType) {
contentType().isEqualTo(contentType);
return this.myself;
}
/**
* Verify that the response's {@code Content-Type} is
* {@linkplain MediaType#isCompatibleWith(MediaType) compatible} with the
* given value.
* @param contentType the expected compatible content type
*/
public WebTestClientResponseAssert hasContentTypeCompatibleWith(MediaType contentType) {
contentType().isCompatibleWith(contentType);
return this.myself;
}
/**
* Verify that the response's {@code Content-Type} is
* {@linkplain MediaType#isCompatibleWith(MediaType) compatible} with the
* given string representation.
* @param contentType the expected compatible content type
*/
public WebTestClientResponseAssert hasContentTypeCompatibleWith(String contentType) {
contentType().isCompatibleWith(contentType);
return this.myself;
}
/**
* Return a new {@linkplain ResponseCookieMapAssert assertion} object that uses the
* response's {@linkplain ResponseCookie cookies} as the object to test.
*/
public ResponseCookieMapAssert cookies() {
return new ResponseCookieMapAssert(getCookies());
}
private ResponseCookie[] getCookies() {
return getExchangeResult().getResponseCookies().values().stream()
.flatMap(Collection::stream)
.toArray(ResponseCookie[]:: new);
}
/**
* 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(getExchangeResult().getResponseBodyContent());
}
/**
* 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).bodyText().isEqualTo("Hello World");
* </code></pre>
*/
public AbstractStringAssert<?> bodyText() {
return Assertions.assertThat(readBody());
}
/**
* Verify that the response body is equal to the given value.
*/
public WebTestClientResponseAssert hasBodyTextEqualTo(String bodyText) {
bodyText().isEqualTo(bodyText);
return this.myself;
}
/**
* 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(new JsonContent(readBody(), getExchangeResult().getJsonConverterDelegate()));
}
private String readBody() {
return new String(getExchangeResult().getResponseBodyContent(), getCharset());
}
private Charset getCharset() {
ExchangeResult result = getExchangeResult();
MediaType contentType = result.getResponseHeaders().getContentType();
Charset charset = (contentType != null ? contentType.getCharset() : null);
return (charset != null ? charset : StandardCharsets.UTF_8);
}
private ExchangeResult getExchangeResult() {
return this.actual.getExchangeResult();
}
}

7
spring-test/src/main/java/org/springframework/test/web/reactive/server/assertj/package-info.java

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
/**
* AssertJ support for WebTestClient.
*/
@NullMarked
package org.springframework.test.web.reactive.server.assertj;
import org.jspecify.annotations.NullMarked;

2
spring-test/src/test/java/org/springframework/test/web/reactive/server/WiretapConnectorTests.java

@ -53,7 +53,7 @@ public class WiretapConnectorTests { @@ -53,7 +53,7 @@ public class WiretapConnectorTests {
ClientRequest clientRequest = ClientRequest.create(HttpMethod.GET, URI.create("/test"))
.header(WebTestClient.WEBTESTCLIENT_REQUEST_ID, "1").build();
WiretapConnector wiretapConnector = new WiretapConnector(connector);
WiretapConnector wiretapConnector = new WiretapConnector(connector, null);
ExchangeFunction function = ExchangeFunctions.create(wiretapConnector);
function.exchange(clientRequest).block(ofMillis(0));

176
spring-test/src/test/java/org/springframework/test/web/reactive/server/assertj/ResponseCookieMapAssertTests.java

@ -0,0 +1,176 @@ @@ -0,0 +1,176 @@
/*
* Copyright 2002-present 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.reactive.server.assertj;
import java.time.Duration;
import java.util.List;
import org.assertj.core.api.AssertProvider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.http.ResponseCookie;
import org.springframework.test.web.servlet.assertj.CookieMapAssert;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link CookieMapAssert}.
*
* @author Rossen Stoyanchev
*/
class ResponseCookieMapAssertTests {
static ResponseCookie[] cookies;
@BeforeAll
static void setup() {
ResponseCookie framework = ResponseCookie.from("framework", "spring").secure(true).httpOnly(true).build();
ResponseCookie age = ResponseCookie.from("age", "value").maxAge(1200).build();
ResponseCookie domain = ResponseCookie.from("domain", "value").domain("spring.io").build();
ResponseCookie path = ResponseCookie.from("path", "value").path("/spring").build();
cookies = List.of(framework, age, domain, path).toArray(new ResponseCookie[0]);
}
@Test
void containsCookieWhenCookieExistsShouldPass() {
assertThat(cookies()).containsCookie("framework");
}
@Test
void containsCookieWhenCookieMissingShouldFail() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(cookies()).containsCookie("missing"));
}
@Test
void containsCookiesWhenCookiesExistShouldPass() {
assertThat(cookies()).containsCookies("framework", "age");
}
@Test
void containsCookiesWhenCookieMissingShouldFail() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(cookies()).containsCookies("framework", "missing"));
}
@Test
void doesNotContainCookieWhenCookieMissingShouldPass() {
assertThat(cookies()).doesNotContainCookie("missing");
}
@Test
void doesNotContainCookieWhenCookieExistsShouldFail() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(cookies()).doesNotContainCookie("framework"));
}
@Test
void doesNotContainCookiesWhenCookiesMissingShouldPass() {
assertThat(cookies()).doesNotContainCookies("missing", "missing2");
}
@Test
void doesNotContainCookiesWhenAtLeastOneCookieExistShouldFail() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(cookies()).doesNotContainCookies("missing", "framework"));
}
@Test
void hasValueEqualsWhenCookieValueMatchesShouldPass() {
assertThat(cookies()).hasValue("framework", "spring");
}
@Test
void hasValueEqualsWhenCookieValueDiffersShouldFail() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(cookies()).hasValue("framework", "other"));
}
@Test
void hasCookieSatisfyingWhenCookieValueMatchesShouldPass() {
assertThat(cookies()).hasCookieSatisfying("framework", cookie -> assertThat(cookie.getValue()).startsWith("spr"));
}
@Test
void hasCookieSatisfyingWhenCookieValueDiffersShouldFail() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(cookies()).hasCookieSatisfying(
"framework", cookie -> assertThat(cookie.getValue()).startsWith("not")));
}
@Test
void hasMaxAgeWhenCookieAgeMatchesShouldPass() {
assertThat(cookies()).hasMaxAge("age", Duration.ofMinutes(20));
}
@Test
void hasMaxAgeWhenCookieAgeDiffersShouldFail() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(cookies()).hasMaxAge("age", Duration.ofMinutes(30)));
}
@Test
void pathWhenCookiePathMatchesShouldPass() {
assertThat(cookies()).hasPath("path", "/spring");
}
@Test
void pathWhenCookiePathDiffersShouldFail() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(cookies()).hasPath("path", "/other"));
}
@Test
void hasDomainWhenCookieDomainMatchesShouldPass() {
assertThat(cookies()).hasDomain("domain", "spring.io");
}
@Test
void hasDomainWhenCookieDomainDiffersShouldFail() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(cookies()).hasDomain("domain", "example.org"));
}
@Test
void isSecureWhenCookieSecureMatchesShouldPass() {
assertThat(cookies()).isSecure("framework", true);
}
@Test
void isSecureWhenCookieSecureDiffersShouldFail() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(cookies()).isSecure("domain", true));
}
@Test
void isHttpOnlyWhenCookieHttpOnlyMatchesShouldPass() {
assertThat(cookies()).isHttpOnly("framework", true);
}
@Test
void isHttpOnlyWhenCookieHttpOnlyDiffersShouldFail() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(cookies()).isHttpOnly("domain", true));
}
private static AssertProvider<ResponseCookieMapAssert> cookies() {
return () -> new ResponseCookieMapAssert(cookies);
}
}

126
spring-test/src/test/java/org/springframework/test/web/reactive/server/assertj/WebTestClientResponseTests.java

@ -0,0 +1,126 @@ @@ -0,0 +1,126 @@
/*
* Copyright 2002-present 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.reactive.server.assertj;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link WebTestClientResponse}.
*
* @author Rossen Stoyanchev
*/
public class WebTestClientResponseTests {
private final WebTestClient client = WebTestClient.bindToController(HelloController.class).build();
@Test
void status() {
ResponseSpec spec = client.get().uri("/greeting").exchange();
assertThat(WebTestClientResponse.from(spec)).hasStatusOk().hasStatus2xxSuccessful();
}
@Test
void headers() {
ResponseSpec spec = client.get().uri("/greeting").exchange();
WebTestClientResponse response = WebTestClientResponse.from(spec);
assertThat(response).hasStatusOk();
assertThat(response).headers()
.containsOnlyHeaders(HttpHeaders.CONTENT_TYPE, HttpHeaders.CONTENT_LENGTH)
.hasValue(HttpHeaders.CONTENT_TYPE, "text/plain;charset=UTF-8")
.hasValue(HttpHeaders.CONTENT_LENGTH, 11);
}
@Test
void contentType() {
ResponseSpec spec = client.get().uri("/greeting").exchange();
WebTestClientResponse response = WebTestClientResponse.from(spec);
assertThat(response).hasStatusOk();
assertThat(response).contentType().isEqualTo("text/plain;charset=UTF-8");
assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN);
}
@Test
void cookies() {
ResponseSpec spec = client.get().uri("/cookie").exchange();
WebTestClientResponse response = WebTestClientResponse.from(spec);
assertThat(response).hasStatusOk();
assertThat(response).cookies().hasValue("foo", "bar");
assertThat(response).body().isEmpty();
}
@Test
void bodyText() {
ResponseSpec spec = client.get().uri("/greeting").exchange();
WebTestClientResponse response = WebTestClientResponse.from(spec);
assertThat(response).hasStatusOk();
assertThat(response).contentType().isCompatibleWith(MediaType.TEXT_PLAIN);
assertThat(response).bodyText().isEqualTo("Hello World");
assertThat(response).hasBodyTextEqualTo("Hello World");
}
@Test
void bodyJson() {
ResponseSpec spec = client.get().uri("/message").exchange();
WebTestClientResponse response = WebTestClientResponse.from(spec);
assertThat(response).hasStatusOk();
assertThat(response).contentType().isEqualTo(MediaType.APPLICATION_JSON);
assertThat(response).bodyJson().extractingPath("$.message").asString().isEqualTo("Hello World");
}
@SuppressWarnings("unused")
@RestController
private static class HelloController {
@GetMapping("/greeting")
public String getGreeting() {
return "Hello World";
}
@GetMapping("/message")
public Map<String, ?> getMessage() {
return Map.of("message", "Hello World");
}
@GetMapping("/cookie")
public void getCookie(ServerWebExchange exchange) {
ResponseCookie cookie = ResponseCookie.from("foo", "bar").build();
exchange.getResponse().addCookie(cookie);
}
}
}
Loading…
Cancel
Save