Refactor WebTestClient assertions take 3

WebTestClient now defines all the steps from setup to performing
an exchange and applying expectations.

The order of expectations now ensures the response status and headers
are verified first since that's available before the body is consumed
and also because it determines how the body is to be decoded, i.e.
error vs success scenarios.

There is now a built-in option for verifying the response as a Map
along with Map-specific assertions.

There are similar options for verifying the response as a List as well
as whether to "collect" the list or "take" the first N elements from
the response stream.
This commit is contained in:
Rossen Stoyanchev 2017-02-17 22:58:32 -05:00
parent f1653cc21c
commit 9829a62044
16 changed files with 588 additions and 410 deletions

View File

@ -19,6 +19,7 @@ import java.net.URI;
import java.nio.charset.Charset;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -26,6 +27,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@ -34,10 +36,10 @@ import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.test.util.AssertionErrors;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserter;
@ -46,6 +48,9 @@ import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriBuilder;
import static org.springframework.test.util.AssertionErrors.assertEquals;
import static org.springframework.test.util.AssertionErrors.assertTrue;
import static org.springframework.test.util.AssertionErrors.fail;
import static org.springframework.web.reactive.function.BodyExtractors.toDataBuffers;
import static org.springframework.web.reactive.function.BodyExtractors.toFlux;
import static org.springframework.web.reactive.function.BodyExtractors.toMono;
@ -242,88 +247,301 @@ class DefaultWebTestClient implements WebTestClient {
@Override
public ResponseSpec exchange() {
return new DefaultResponseSpec(this.requestId, this.headerSpec.exchange());
return createResponseSpec(this.headerSpec.exchange());
}
@Override
public <T> ResponseSpec exchange(BodyInserter<T, ? super ClientHttpRequest> inserter) {
return new DefaultResponseSpec(this.requestId, this.headerSpec.exchange(inserter));
return createResponseSpec(this.headerSpec.exchange(inserter));
}
@Override
public <T, S extends Publisher<T>> ResponseSpec exchange(S publisher, Class<T> elementClass) {
return new DefaultResponseSpec(this.requestId, this.headerSpec.exchange(publisher, elementClass));
return createResponseSpec(this.headerSpec.exchange(publisher, elementClass));
}
protected DefaultResponseSpec createResponseSpec(Mono<ClientResponse> responseMono) {
ClientResponse response = responseMono.block(getTimeout());
WiretapConnector.Info info = connectorListener.retrieveRequest(this.requestId);
HttpMethod method = info.getMethod();
URI url = info.getUrl();
HttpHeaders headers = info.getRequestHeaders();
ExchangeResult<Flux<DataBuffer>> result = ExchangeResult.fromResponse(method, url, headers, response);
return new DefaultResponseSpec(result, response);
}
}
private abstract class ResponseSpecSupport {
private final ExchangeResult<Flux<DataBuffer>> exchangeResult;
private final ClientResponse response;
public ResponseSpecSupport(ExchangeResult<Flux<DataBuffer>> result, ClientResponse response) {
this.exchangeResult = result;
this.response = response;
}
public ResponseSpecSupport(ResponseSpecSupport responseSpec) {
this.exchangeResult = responseSpec.getExchangeResult();
this.response = responseSpec.getResponse();
}
protected ExchangeResult<Flux<DataBuffer>> getExchangeResult() {
return this.exchangeResult;
}
protected ClientResponse getResponse() {
return this.response;
}
protected HttpHeaders getResponseHeaders() {
return getExchangeResult().getResponseHeaders();
}
protected <T> ExchangeResult<T> createResultWithDecodedBody(T body) {
return ExchangeResult.withDecodedBody(this.exchangeResult, body);
}
}
private class DefaultResponseSpec extends ResponseSpecSupport implements ResponseSpec {
public DefaultResponseSpec(ExchangeResult<Flux<DataBuffer>> result, ClientResponse response) {
super(result, response);
}
@Override
public StatusAssertions expectStatus() {
return new StatusAssertions(getResponse().statusCode(), this);
}
@Override
public HeaderAssertions expectHeader() {
return new HeaderAssertions(getResponseHeaders(), this);
}
@Override
public BodySpec expectBody() {
return new DefaultBodySpec(this);
}
@Override
public ElementBodySpec expectBody(Class<?> elementType) {
return expectBody(ResolvableType.forClass(elementType));
}
@Override
public ElementBodySpec expectBody(ResolvableType elementType) {
return new DefaultElementBodySpec(this, elementType);
}
@Override
public ResponseSpec consumeWith(Consumer<ExchangeResult<Flux<DataBuffer>>> consumer) {
consumer.accept(getExchangeResult());
return this;
}
@Override
public ExchangeResult<Flux<DataBuffer>> returnResult() {
return getExchangeResult();
}
}
private class DefaultResponseSpec implements ResponseSpec {
private final String requestId;
private final Mono<ClientResponse> responseMono;
private class DefaultBodySpec extends ResponseSpecSupport implements BodySpec {
public DefaultResponseSpec(String requestId, Mono<ClientResponse> responseMono) {
this.requestId = requestId;
this.responseMono = responseMono;
public DefaultBodySpec(ResponseSpecSupport responseSpec) {
super(responseSpec);
}
@Override
public <T> ExchangeResult<T> decodeEntity(Class<T> entityClass) {
return decodeEntity(ResolvableType.forClass(entityClass));
public ExchangeResult<Void> isEmpty() {
DataBuffer buffer = getResponse().body(toDataBuffers()).blockFirst(getTimeout());
assertTrue("Expected empty body", buffer == null);
return createResultWithDecodedBody(null);
}
@Override
public <T> ExchangeResult<List<T>> decodeAndCollect(Class<T> elementClass) {
return decodeAndCollect(ResolvableType.forClass(elementClass));
public MapBodySpec map(Class<?> keyType, Class<?> valueType) {
return map(ResolvableType.forClass(keyType), ResolvableType.forClass(valueType));
}
@Override
public <T> ExchangeResult<Flux<T>> decodeFlux(Class<T> elementClass) {
return decodeFlux(ResolvableType.forClass(elementClass));
public MapBodySpec map(ResolvableType keyType, ResolvableType valueType) {
return new DefaultMapBodySpec(this, keyType, valueType);
}
}
private class DefaultMapBodySpec extends ResponseSpecSupport implements MapBodySpec {
private final Map<?, ?> body;
public DefaultMapBodySpec(ResponseSpecSupport spec, ResolvableType keyType, ResolvableType valueType) {
super(spec);
ResolvableType mapType = ResolvableType.forClassWithGenerics(Map.class, keyType, valueType);
this.body = (Map<?, ?>) spec.getResponse().body(toMono(mapType)).block(getTimeout());
}
@Override
public <K, V> ExchangeResult<Map<K, V>> isEqualTo(Map<K, V> expected) {
return returnResult();
}
@Override
public <T> ExchangeResult<T> decodeEntity(ResolvableType elementType) {
return this.responseMono.then(response -> {
Mono<T> entityMono = response.body(toMono(elementType));
return entityMono.map(entity -> createTestExchange(entity, response));
}).block(getTimeout());
public MapBodySpec hasSize(int size) {
assertEquals("Response body map size", size, this.body.size());
return this;
}
@Override
public <T> ExchangeResult<List<T>> decodeAndCollect(ResolvableType elementType) {
return this.responseMono.then(response -> {
Flux<T> entityFlux = response.body(toFlux(elementType));
return entityFlux.collectList().map(list -> createTestExchange(list, response));
}).block(getTimeout());
public MapBodySpec contains(Object key, Object value) {
assertEquals("Response body map value for key " + key, value, this.body.get(key));
return this;
}
@Override
public <T> ExchangeResult<Flux<T>> decodeFlux(ResolvableType elementType) {
return this.responseMono.map(response -> {
Flux<T> entityFlux = response.body(toFlux(elementType));
return createTestExchange(entityFlux, response);
}).block(getTimeout());
public MapBodySpec containsKeys(Object... keys) {
List<Object> missing = Arrays.stream(keys)
.filter(key -> !this.body.containsKey(key))
.collect(Collectors.toList());
if (!missing.isEmpty()) {
fail("Response body map does not contain keys " + Arrays.toString(keys));
}
return this;
}
@Override
public ExchangeResult<Void> expectNoBody() {
return this.responseMono.map(response -> {
DataBuffer buffer = response.body(toDataBuffers()).blockFirst(getTimeout());
AssertionErrors.assertTrue("Expected empty body", buffer == null);
ExchangeResult<Void> exchange = createTestExchange(null, response);
return exchange;
}).block(getTimeout());
public MapBodySpec containsValues(Object... values) {
List<Object> missing = Arrays.stream(values)
.filter(value -> !this.body.containsValue(value))
.collect(Collectors.toList());
if (!missing.isEmpty()) {
fail("Response body map does not contain values " + Arrays.toString(values));
}
return this;
}
private <T> ExchangeResult<T> createTestExchange(T body, ClientResponse response) {
WiretapConnector.Info wiretapInfo = connectorListener.retrieveRequest(requestId);
ClientHttpRequest request = wiretapInfo.getRequest();
return new ExchangeResult<T>(
request.getMethod(), request.getURI(), request.getHeaders(),
response.statusCode(), response.headers().asHttpHeaders(), body);
@Override
@SuppressWarnings("unchecked")
public <K, V> ExchangeResult<Map<K, V>> returnResult() {
return createResultWithDecodedBody((Map<K, V>) this.body);
}
}
private class DefaultElementBodySpec extends ResponseSpecSupport implements ElementBodySpec {
private final ResolvableType elementType;
public DefaultElementBodySpec(ResponseSpecSupport spec, ResolvableType elementType) {
super(spec);
this.elementType = elementType;
}
@Override
public SingleValueBodySpec value() {
return new DefaultSingleValueBodySpec(this, this.elementType);
}
@Override
public ListBodySpec list() {
return new DefaultListBodySpec(this, this.elementType, -1);
}
@Override
public ListBodySpec list(int elementCount) {
return new DefaultListBodySpec(this, this.elementType, elementCount);
}
@Override
public <T> ExchangeResult<Flux<T>> returnResult() {
Flux<T> flux = getResponse().body(toFlux(this.elementType));
return createResultWithDecodedBody(flux);
}
}
private class DefaultSingleValueBodySpec extends ResponseSpecSupport
implements SingleValueBodySpec {
private final Object body;
public DefaultSingleValueBodySpec(ResponseSpecSupport spec, ResolvableType elementType) {
super(spec);
this.body = getResponse().body(toMono(elementType)).block(getTimeout());
}
@Override
public <T> ExchangeResult<T> isEqualTo(Object expected) {
assertEquals("Response body", expected, this.body);
return returnResult();
}
@Override
@SuppressWarnings("unchecked")
public <T> ExchangeResult<T> returnResult() {
return createResultWithDecodedBody((T) this.body);
}
}
private class DefaultListBodySpec extends ResponseSpecSupport
implements ListBodySpec {
private final List<?> body;
public DefaultListBodySpec(ResponseSpecSupport spec, ResolvableType elementType, int elementCount) {
super(spec);
Flux<?> flux = getResponse().body(toFlux(elementType));
if (elementCount >= 0) {
flux = flux.take(elementCount);
}
this.body = flux.collectList().block(getTimeout());
}
@Override
public <T> ExchangeResult<List<T>> isEqualTo(List<T> expected) {
assertEquals("Response body", expected, this.body);
return returnResult();
}
@Override
public ListBodySpec hasSize(int size) {
return this;
}
@Override
public ListBodySpec contains(Object... elements) {
List<Object> elementList = Arrays.asList(elements);
String message = "Response body does not contain " + elementList;
assertTrue(message, this.body.containsAll(elementList));
return this;
}
@Override
public ListBodySpec doesNotContain(Object... elements) {
List<Object> elementList = Arrays.asList(elements);
String message = "Response body should have contained " + elementList;
assertTrue(message, !this.body.containsAll(Arrays.asList(elements)));
return this;
}
@Override
@SuppressWarnings("unchecked")
public <T> ExchangeResult<List<T>> returnResult() {
return createResultWithDecodedBody((List<T>) this.body);
}
}
@ -346,7 +564,7 @@ class DefaultWebTestClient implements WebTestClient {
@Override
public void accept(WiretapConnector.Info info) {
Optional.ofNullable(info.getRequest().getHeaders().getFirst(REQUEST_ID_HEADER_NAME))
Optional.ofNullable(info.getRequestHeaders().getFirst(REQUEST_ID_HEADER_NAME))
.ifPresent(id -> this.exchanges.put(id, info));
}

View File

@ -18,21 +18,21 @@ package org.springframework.test.web.reactive.server;
import java.net.URI;
import java.util.stream.Collectors;
import reactor.core.publisher.Flux;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.client.ClientResponse;
import static org.springframework.web.reactive.function.BodyExtractors.toDataBuffers;
/**
* Container for request and response details including the decoded response
* body from an exchange performed through {@link WebTestClient}.
* Container for request and response details from an exchange performed
* through {@link WebTestClient}.
*
* <p>Use {@link #assertThat()} to access built-in assertions on the response,
* or apply other assertions directly to the data contained in this class.
* The built-in assertions provide an option for logging diagnostic information
* about the exchange. The same can also be obtained using the
* {@link #toString()} method of this class.
*
* @param <T> the type of the decoded response body
* @param <T> the type of the response body
*
* @author Rossen Stoyanchev
* @since 5.0
@ -52,8 +52,11 @@ public class ExchangeResult<T> {
private final T responseBody;
public ExchangeResult(HttpMethod method, URI url, HttpHeaders requestHeaders,
HttpStatus status, HttpHeaders responseHeaders, T responseBody) {
/**
* Package private constructor.
*/
ExchangeResult(HttpMethod method, URI url, HttpHeaders requestHeaders, HttpStatus status,
HttpHeaders responseHeaders, T responseBody) {
this.method = method;
this.url = url;
@ -64,25 +67,17 @@ public class ExchangeResult<T> {
}
/**
* Provides access to built-in assertions on the response.
*/
public ResponseAssertions<T> assertThat() {
return new ResponseAssertions<T>(this);
}
/**
* Return the request method of the exchange.
*/
public HttpMethod getRequestMethod() {
public HttpMethod getMethod() {
return this.method;
}
/**
* Return the URL of the exchange.
*/
public URI getRequestUrl() {
public URI getUrl() {
return this.url;
}
@ -96,7 +91,7 @@ public class ExchangeResult<T> {
/**
* Return the response status.
*/
public HttpStatus getResponseStatus() {
public HttpStatus getStatus() {
return this.status;
}
@ -115,11 +110,32 @@ public class ExchangeResult<T> {
}
/**
* Create an instance from a ClientResponse (body not yet consumed).
*/
static ExchangeResult<Flux<DataBuffer>> fromResponse(HttpMethod method, URI url,
HttpHeaders requestHeaders, ClientResponse response) {
HttpStatus status = response.statusCode();
HttpHeaders responseHeaders = response.headers().asHttpHeaders();
Flux<DataBuffer> body = response.body(toDataBuffers());
return new ExchangeResult<>(method, url, requestHeaders, status, responseHeaders, body);
}
/**
* Re-create the result with a generic type matching the decoded body.
*/
static <T> ExchangeResult<T> withDecodedBody(ExchangeResult<?> result, T body) {
return new ExchangeResult<>(result.getMethod(), result.getUrl(), result.getRequestHeaders(),
result.getStatus(), result.getResponseHeaders(), body);
}
@Override
public String toString() {
HttpStatus status = this.status;
return "\n\n" +
formatValue("Request", this.method + " " + getRequestUrl()) +
formatValue("Request", this.method + " " + getUrl()) +
formatValue("Status", status + " " + status.getReasonPhrase()) +
formatHeading("Response Headers") +
formatHeaders(this.responseHeaders) +

View File

@ -35,68 +35,68 @@ import static org.springframework.test.util.AssertionErrors.assertTrue;
* @since 5.0
* @see ResponseAssertions#header()
*/
public class HeaderAssertions<T> {
private final ResponseAssertions<T> resultAssertions;
public class HeaderAssertions {
private final HttpHeaders headers;
private final WebTestClient.ResponseSpec responseSpec;
public HeaderAssertions(HttpHeaders headers, ResponseAssertions<T> resultAssertions) {
this.resultAssertions = resultAssertions;
public HeaderAssertions(HttpHeaders headers, WebTestClient.ResponseSpec responseSpec) {
this.headers = headers;
this.responseSpec = responseSpec;
}
public ResponseAssertions<T> valueEquals(String headerName, String... values) {
public WebTestClient.ResponseSpec valueEquals(String headerName, String... values) {
List<String> actual = this.headers.get(headerName);
assertEquals("Response header [" + headerName + "]", Arrays.asList(values), actual);
return this.resultAssertions;
return this.responseSpec;
}
public ResponseAssertions<T> valueMatches(String headerName, String pattern) {
public WebTestClient.ResponseSpec valueMatches(String headerName, String pattern) {
List<String> values = this.headers.get(headerName);
String value = CollectionUtils.isEmpty(values) ? "" : values.get(0);
boolean match = Pattern.compile(pattern).matcher(value).matches();
String message = "Response header " + headerName + "=\'" + value + "\' does not match " + pattern;
assertTrue(message, match);
return this.resultAssertions;
return this.responseSpec;
}
public ResponseAssertions<T> cacheControlEquals(CacheControl cacheControl) {
public WebTestClient.ResponseSpec cacheControlEquals(CacheControl cacheControl) {
String actual = this.headers.getCacheControl();
assertEquals("Response header Cache-Control", cacheControl.getHeaderValue(), actual);
return this.resultAssertions;
return this.responseSpec;
}
public ResponseAssertions<T> contentDispositionEquals(ContentDisposition contentDisposition) {
public WebTestClient.ResponseSpec contentDispositionEquals(ContentDisposition contentDisposition) {
ContentDisposition actual = this.headers.getContentDisposition();
assertEquals("Response header Content-Disposition", contentDisposition, actual);
return this.resultAssertions;
return this.responseSpec;
}
public ResponseAssertions<T> contentLengthEquals(long contentLength) {
public WebTestClient.ResponseSpec contentLengthEquals(long contentLength) {
long actual = this.headers.getContentLength();
assertEquals("Response header Content-Length", contentLength, actual);
return this.resultAssertions;
return this.responseSpec;
}
public ResponseAssertions<T> contentTypeEquals(MediaType mediaType) {
public WebTestClient.ResponseSpec contentTypeEquals(MediaType mediaType) {
MediaType actual = this.headers.getContentType();
assertEquals("Response header Content-Type", mediaType, actual);
return this.resultAssertions;
return this.responseSpec;
}
public ResponseAssertions<T> expiresEquals(int expires) {
public WebTestClient.ResponseSpec expiresEquals(int expires) {
long actual = this.headers.getExpires();
assertEquals("Response header Expires", expires, actual);
return this.resultAssertions;
return this.responseSpec;
}
public ResponseAssertions<T> lastModifiedEquals(int lastModified) {
public WebTestClient.ResponseSpec lastModifiedEquals(int lastModified) {
long actual = this.headers.getLastModified();
assertEquals("Response header Last-Modified", lastModified, actual);
return this.resultAssertions;
return this.responseSpec;
}
}

View File

@ -1,110 +0,0 @@
/*
* Copyright 2002-2017 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
*
* http://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;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Provides options for logging diagnostic information about the exchange.
*
* @author Rossen Stoyanchev
* @since 5.0
* @see ResponseAssertions#log()
*/
public class LoggingActions<T> {
private static Log logger = LogFactory.getLog(LoggingActions.class);
private final ResponseAssertions<T> resultAssertions;
private final ExchangeResult<T> exchange;
public LoggingActions(ExchangeResult<T> exchange, ResponseAssertions<T> resultAssertions) {
this.resultAssertions = resultAssertions;
this.exchange = exchange;
}
/**
* Log with {@link System#out}.
*/
public ResponseAssertions<T> toConsole() {
return toOutputStream(System.out);
}
/**
* Log with a given {@link OutputStream}.
*/
public ResponseAssertions<T> toOutputStream(OutputStream stream) {
return toWriter(new PrintWriter(stream, true));
}
/**
* Log with a given {@link Writer}.
*/
public ResponseAssertions<T> toWriter(Writer writer) {
try {
writer.write(getOutput());
}
catch (IOException ex) {
throw new IllegalStateException("Failed to print exchange info", ex);
}
return this.resultAssertions;
}
/**
* Log if TRACE level logging is enabled.
*/
public ResponseAssertions<T> ifTraceEnabled() {
return doLog(Log::isTraceEnabled, Log::trace);
}
/**
* Log if DEBUG level logging is enabled.
*/
public ResponseAssertions<T> ifDebugEnabled() {
return doLog(Log::isDebugEnabled, Log::debug);
}
/**
* Log if INFO level logging is enabled.
*/
public ResponseAssertions<T> ifInfoEnabled() {
return doLog(Log::isInfoEnabled, Log::info);
}
private ResponseAssertions<T> doLog(Predicate<Log> predicate, BiConsumer<Log, String> consumer) {
if (predicate.test(logger)) {
consumer.accept(logger, getOutput());
}
return this.resultAssertions;
}
private String getOutput() {
return this.resultAssertions.toString();
}
}

View File

@ -1,66 +0,0 @@
/*
* Copyright 2002-2017 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
*
* http://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;
import static org.springframework.test.util.AssertionErrors.assertEquals;
/**
* Assertions on an {@code ExchangeResult}.
*
* <p>Use {@link ExchangeResult#assertThat()} to access these assertions.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ResponseAssertions<T> {
private final ExchangeResult<T> exchangeResult;
ResponseAssertions(ExchangeResult<T> exchangeResult) {
this.exchangeResult = exchangeResult;
}
/**
* Assertions on the response status.
*/
public StatusAssertions<T> status() {
return new StatusAssertions<>(this.exchangeResult.getResponseStatus(), this);
}
/**
* Assertions on response headers.
*/
public HeaderAssertions<T> header() {
return new HeaderAssertions<>(this.exchangeResult.getResponseHeaders(), this);
}
/**
* Assert the response body is equal to the given value.
*/
public void bodyEquals(T value) {
assertEquals("Response body", value, this.exchangeResult.getResponseBody());
}
/**
* Options for logging diagnostic information.
*/
public LoggingActions<T> log() {
return new LoggingActions<T>(this.exchangeResult, this);
}
}

View File

@ -27,174 +27,174 @@ import static org.springframework.test.util.AssertionErrors.assertEquals;
* @see ResponseAssertions#status()
*/
@SuppressWarnings("unused")
public class StatusAssertions<T> {
private final ResponseAssertions<T> resultAssertions;
public class StatusAssertions {
private final HttpStatus httpStatus;
private final WebTestClient.ResponseSpec responseSpec;
StatusAssertions(HttpStatus status, ResponseAssertions<T> exchangeActions) {
this.resultAssertions = exchangeActions;
StatusAssertions(HttpStatus status, WebTestClient.ResponseSpec responseSpec) {
this.httpStatus = status;
this.responseSpec = responseSpec;
}
/**
* Assert the response status as an {@link HttpStatus}.
*/
public ResponseAssertions<T> isEqualTo(HttpStatus status) {
public WebTestClient.ResponseSpec isEqualTo(HttpStatus status) {
assertEquals("Response status", status, this.httpStatus);
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response status as an integer.
*/
public ResponseAssertions<T> isEqualTo(int status) {
public WebTestClient.ResponseSpec isEqualTo(int status) {
assertEquals("Response status", status, this.httpStatus.value());
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response status code is {@code HttpStatus.OK} (200).
*/
public ResponseAssertions<T> isOk() {
public WebTestClient.ResponseSpec isOk() {
assertEquals("Status", HttpStatus.OK, this.httpStatus);
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response status code is {@code HttpStatus.CREATED} (201).
*/
public ResponseAssertions<T> isCreated() {
public WebTestClient.ResponseSpec isCreated() {
assertEquals("Status", HttpStatus.CREATED, this.httpStatus);
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response status code is {@code HttpStatus.ACCEPTED} (202).
*/
public ResponseAssertions<T> isAccepted() {
public WebTestClient.ResponseSpec isAccepted() {
assertEquals("Status", HttpStatus.ACCEPTED, this.httpStatus);
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response status code is {@code HttpStatus.NO_CONTENT} (204).
*/
public ResponseAssertions<T> isNoContent() {
public WebTestClient.ResponseSpec isNoContent() {
assertEquals("Status", HttpStatus.NO_CONTENT, this.httpStatus);
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response status code is {@code HttpStatus.FOUND} (302).
*/
public ResponseAssertions<T> isFound() {
public WebTestClient.ResponseSpec isFound() {
assertEquals("Status", HttpStatus.FOUND, this.httpStatus);
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response status code is {@code HttpStatus.SEE_OTHER} (303).
*/
public ResponseAssertions<T> isSeeOther() {
public WebTestClient.ResponseSpec isSeeOther() {
assertEquals("Status", HttpStatus.SEE_OTHER, this.httpStatus);
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response status code is {@code HttpStatus.NOT_MODIFIED} (304).
*/
public ResponseAssertions<T> isNotModified() {
public WebTestClient.ResponseSpec isNotModified() {
assertEquals("Status", HttpStatus.NOT_MODIFIED, this.httpStatus);
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response status code is {@code HttpStatus.TEMPORARY_REDIRECT} (307).
*/
public ResponseAssertions<T> isTemporaryRedirect() {
public WebTestClient.ResponseSpec isTemporaryRedirect() {
assertEquals("Status", HttpStatus.TEMPORARY_REDIRECT, this.httpStatus);
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response status code is {@code HttpStatus.PERMANENT_REDIRECT} (308).
*/
public ResponseAssertions<T> isPermanentRedirect() {
public WebTestClient.ResponseSpec isPermanentRedirect() {
assertEquals("Status", HttpStatus.PERMANENT_REDIRECT, this.httpStatus);
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response status code is {@code HttpStatus.BAD_REQUEST} (400).
*/
public ResponseAssertions<T> isBadRequest() {
public WebTestClient.ResponseSpec isBadRequest() {
assertEquals("Status", HttpStatus.BAD_REQUEST, this.httpStatus);
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response status code is {@code HttpStatus.NOT_FOUND} (404).
*/
public ResponseAssertions<T> isNotFound() {
public WebTestClient.ResponseSpec isNotFound() {
assertEquals("Status", HttpStatus.NOT_FOUND, this.httpStatus);
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response error message.
*/
public ResponseAssertions<T> reasonEquals(String reason) {
public WebTestClient.ResponseSpec reasonEquals(String reason) {
assertEquals("Response status reason", reason, this.httpStatus.getReasonPhrase());
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response status code is in the 1xx range.
*/
public ResponseAssertions<T> is1xxInformational() {
public WebTestClient.ResponseSpec is1xxInformational() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.INFORMATIONAL, this.httpStatus.series());
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response status code is in the 2xx range.
*/
public ResponseAssertions<T> is2xxSuccessful() {
public WebTestClient.ResponseSpec is2xxSuccessful() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.SUCCESSFUL, this.httpStatus.series());
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response status code is in the 3xx range.
*/
public ResponseAssertions<T> is3xxRedirection() {
public WebTestClient.ResponseSpec is3xxRedirection() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.REDIRECTION, this.httpStatus.series());
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response status code is in the 4xx range.
*/
public ResponseAssertions<T> is4xxClientError() {
public WebTestClient.ResponseSpec is4xxClientError() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.CLIENT_ERROR, this.httpStatus.series());
return this.resultAssertions;
return this.responseSpec;
}
/**
* Assert the response status code is in the 5xx range.
*/
public ResponseAssertions<T> is5xxServerError() {
public WebTestClient.ResponseSpec is5xxServerError() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.SERVER_ERROR, this.httpStatus.series());
return this.resultAssertions;
return this.responseSpec;
}
}

View File

@ -29,6 +29,7 @@ import reactor.core.publisher.Flux;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@ -447,72 +448,191 @@ public interface WebTestClient {
}
/**
* Specification for decoding the response of an exchange.
* Specification for expectations on the response.
*/
interface ResponseSpec {
/**
* Decode the response as a single entity of the given type.
* @param entityClass the entity type
* @param <T> the type of entity
* @return container for the result of the exchange
* Assertions on the response status.
*/
<T> ExchangeResult<T> decodeEntity(Class<T> entityClass);
StatusAssertions expectStatus();
/**
* Alternative to {@link #decodeEntity(Class)} useful for entity
* types with generics.
* @param entityType the type of entity
* @param <T> the type of entity
* @return container for the result of the exchange
* @see ResolvableType#forClassWithGenerics(Class, Class[])
* Assertions on the response headers.
*/
<T> ExchangeResult<T> decodeEntity(ResolvableType entityType);
HeaderAssertions expectHeader();
/**
* Decode the response as a finite stream of the given element type and
* collect the items into a list.
* @param elementClass the list element type
* @param <T> the type of list elements
* @return container for the result of the exchange
* Assertions on the response body.
*/
<T> ExchangeResult<List<T>> decodeAndCollect(Class<T> elementClass);
BodySpec expectBody();
/**
* Alternative to {@link #decodeAndCollect(Class)} useful for element
* types with generics.
* @param elementType the list element type
* @param <T> the type of list elements
* @return container for the result of the exchange
* @see ResolvableType#forClassWithGenerics(Class, Class[])
* Assertions on the response body where the body is to be decoded to
* one or more elements of the given type.
*/
<T> ExchangeResult<List<T>> decodeAndCollect(ResolvableType elementType);
ElementBodySpec expectBody(Class<?> elementType);
/**
* Turn the response stream of byte buffers into a stream of Objects of
* the given type.
* @param elementClass the stream element type
* @param <T> the type of stream elements
* @return container for the result of the exchange
* Alternative to {@link #expectBody(Class)} for generic types.
*/
<T> ExchangeResult<Flux<T>> decodeFlux(Class<T> elementClass);
ElementBodySpec expectBody(ResolvableType elementType);
/**
* Alternative to {@link #decodeFlux(Class)} useful for element types
* with generics.
* @param elementType the stream element type
* @param <T> the type of stream elements
* @return container for the result of the exchange
* @see ResolvableType#forClassWithGenerics(Class, Class[])
* Consume the result of the exchange and continue with expectations.
*/
<T> ExchangeResult<Flux<T>> decodeFlux(ResolvableType elementType);
ResponseSpec consumeWith(Consumer<ExchangeResult<Flux<DataBuffer>>> consumer);
/**
* Consume the response and verify there is no content.
* Return a container for the result of the exchange with the body
* not yet decoded nor consumed.
*/
ExchangeResult<Flux<DataBuffer>> returnResult();
}
/**
* Specification for expectations on the body of the response.
*/
interface BodySpec {
/**
* Consume the body and verify it is empty.
* @return container for the result of the exchange
*/
ExchangeResult<Void> expectNoBody();
ExchangeResult<Void> isEmpty();
/**
* Decode the response body as a Map with the given key and value type.
*/
MapBodySpec map(Class<?> keyType, Class<?> valueType);
/**
* Alternative to {@link #map(Class, Class)} for generic types.
*/
MapBodySpec map(ResolvableType keyType, ResolvableType valueType);
}
/**
* Specification for expectations on the body of the response decoded as a map.
*/
interface MapBodySpec {
/**
* Assert the decoded body is equal to the given list of elements.
*/
<K, V> ExchangeResult<Map<K, V>> isEqualTo(Map<K, V> expected);
/**
* Assert the decoded map has the given size.
* @param size the expected size
*/
MapBodySpec hasSize(int size);
/**
* Assert the decoded map contains the given key value pair.
* @param key the key to check
* @param value the value to check
*/
MapBodySpec contains(Object key, Object value);
/**
* Assert the decoded map contains the given keys.
* @param keys the keys to check
*/
MapBodySpec containsKeys(Object... keys);
/**
* Assert the decoded map contains the given values.
* @param values the keys to check
*/
MapBodySpec containsValues(Object... values);
/**
* Return a container for the result of the exchange.
*/
<K, V> ExchangeResult<Map<K, V>> returnResult();
}
/**
* Specification for expectations on the body of the response to be decoded
* as one or more elements of a specific type.
*/
interface ElementBodySpec {
/**
* Decode the response as a single element.
*/
SingleValueBodySpec value();
/**
* Decode the response as a Flux of objects and collect it to a list.
*/
ListBodySpec list();
/**
* Decode the response as a Flux of objects consuming only the specified
* number of elements.
*/
ListBodySpec list(int elementCount);
/**
* Decode the response as a Flux of objects and return a container for
* the result where the response body not yet consumed.
*/
<T> ExchangeResult<Flux<T>> returnResult();
}
/**
* Specification for expectations on the body of the response decoded as a
* single element of a specific type.
*/
interface SingleValueBodySpec {
/**
* Assert the decoded body is equal to the given value.
*/
<T> ExchangeResult<T> isEqualTo(Object expected);
/**
* Return a container for the result of the exchange.
*/
<T> ExchangeResult<T> returnResult();
}
/**
* Specification for expectations on the body of the response decoded as a
* list of elements of a specific type.
*/
interface ListBodySpec {
/**
* Assert the decoded body is equal to the given list of elements.
*/
<T> ExchangeResult<List<T>> isEqualTo(List<T> expected);
/**
* Assert the decoded list of values is of the given size.
* @param size the expected size
*/
ListBodySpec hasSize(int size);
/**
* Assert the decoded list of values contains the given elements.
* @param elements the elements to check
*/
ListBodySpec contains(Object... elements);
/**
* Assert the decoded list of values does not contain the given elements.
* @param elements the elements to check
*/
ListBodySpec doesNotContain(Object... elements);
/**
* Return a container for the result of the exchange.
*/
<T> ExchangeResult<List<T>> returnResult();
}
}

View File

@ -24,6 +24,7 @@ import java.util.function.Function;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpRequest;
@ -79,19 +80,33 @@ class WiretapConnector implements ClientHttpConnector {
public static class Info {
private final ClientHttpRequest request;
private final HttpMethod method;
private final URI url;
private final HttpHeaders requestHeaders;
private final ClientHttpResponse response;
public Info(ClientHttpRequest request, ClientHttpResponse response) {
this.request = request;
this.method = request.getMethod();
this.url = request.getURI();
this.requestHeaders = request.getHeaders();
this.response = response;
}
public ClientHttpRequest getRequest() {
return this.request;
public HttpMethod getMethod() {
return this.method;
}
public URI getUrl() {
return this.url;
}
public HttpHeaders getRequestHeaders() {
return this.requestHeaders;
}
public ClientHttpResponse getResponse() {

View File

@ -33,6 +33,7 @@ import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import org.springframework.web.reactive.function.client.ExchangeFunctions;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
@ -60,7 +61,8 @@ public class WiretapConnectorTests {
WiretapConnector.Info info = infoRef.get();
assertNotNull(info);
assertSame(request, info.getRequest());
assertEquals(HttpMethod.GET, info.getMethod());
assertEquals("/test", info.getUrl().toString());
assertSame(response, info.getResponse());
}

View File

@ -44,18 +44,16 @@ public class ErrorTests {
public void notFound() throws Exception {
this.client.get().uri("/invalid")
.exchange()
.expectNoBody()
.assertThat()
.status().isNotFound();
.expectStatus().isNotFound()
.expectBody().isEmpty();
}
@Test
public void serverException() throws Exception {
this.client.get().uri("/server-error")
.exchange()
.expectNoBody()
.assertThat()
.status().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
.expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)
.expectBody().isEmpty();
}

View File

@ -49,20 +49,16 @@ public class HeaderTests {
public void requestResponseHeaderPair() throws Exception {
this.client.get().uri("/request-response-pair").header("h1", "in")
.exchange()
.expectNoBody()
.assertThat()
.status().isOk()
.header().valueEquals("h1", "in-out");
.expectStatus().isOk()
.expectHeader().valueEquals("h1", "in-out");
}
@Test
public void headerMultivalue() throws Exception {
this.client.get().uri("/multivalue")
.exchange()
.expectNoBody()
.assertThat()
.status().isOk()
.header().valueEquals("h1", "v1", "v2", "v3");
.expectStatus().isOk()
.expectHeader().valueEquals("h1", "v1", "v2", "v3");
}

View File

@ -18,6 +18,7 @@ package org.springframework.test.web.reactive.server.samples;
import java.net.URI;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonCreator;
@ -28,7 +29,6 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.web.reactive.server.ExchangeResult;
@ -65,22 +65,22 @@ public class ResponseEntityTests {
public void entity() throws Exception {
this.client.get().uri("/persons/John")
.exchange()
.decodeEntity(Person.class)
.assertThat()
.status().isOk()
.header().contentTypeEquals(MediaType.APPLICATION_JSON_UTF8)
.bodyEquals(new Person("John"));
.expectStatus().isOk()
.expectHeader().contentTypeEquals(MediaType.APPLICATION_JSON_UTF8)
.expectBody(Person.class).value().isEqualTo(new Person("John"));
}
@Test
public void entityList() throws Exception {
List<Person> expected = Arrays.asList(
new Person("Jane"), new Person("Jason"), new Person("John"));
this.client.get().uri("/persons")
.exchange()
.decodeAndCollect(Person.class)
.assertThat()
.status().isOk()
.header().contentTypeEquals(MediaType.APPLICATION_JSON_UTF8)
.bodyEquals(Arrays.asList(new Person("Jane"), new Person("Jason"), new Person("John")));
.expectStatus().isOk()
.expectHeader().contentTypeEquals(MediaType.APPLICATION_JSON_UTF8)
.expectBody(Person.class).list().isEqualTo(expected);
}
@Test
@ -93,10 +93,9 @@ public class ResponseEntityTests {
this.client.get().uri("/persons?map=true")
.exchange()
.decodeEntity(ResolvableType.forClassWithGenerics(Map.class, String.class, Person.class))
.assertThat()
.status().isOk()
.bodyEquals(map);
.expectStatus().isOk()
.expectBody()
.map(String.class, Person.class).isEqualTo(map);
}
@Test
@ -106,11 +105,10 @@ public class ResponseEntityTests {
.uri("/persons")
.accept(TEXT_EVENT_STREAM)
.exchange()
.decodeFlux(Person.class);
result.assertThat()
.status().isOk()
.header().contentTypeEquals(TEXT_EVENT_STREAM);
.expectStatus().isOk()
.expectHeader().contentTypeEquals(TEXT_EVENT_STREAM)
.expectBody(Person.class)
.returnResult();
StepVerifier.create(result.getResponseBody())
.expectNext(new Person("N0"), new Person("N1"), new Person("N2"))
@ -124,10 +122,9 @@ public class ResponseEntityTests {
public void postEntity() throws Exception {
this.client.post().uri("/persons")
.exchange(Mono.just(new Person("John")), Person.class)
.expectNoBody()
.assertThat()
.status().isCreated()
.header().valueEquals("location", "/persons/John");
.expectStatus().isCreated()
.expectHeader().valueEquals("location", "/persons/John")
.expectBody().isEmpty();
}

View File

@ -51,10 +51,8 @@ public class ApplicationContextTests {
public void test() throws Exception {
this.client.get().uri("/test")
.exchange()
.decodeEntity(String.class)
.assertThat()
.status().isOk()
.bodyEquals("It works!");
.expectStatus().isOk()
.expectBody(String.class).value().isEqualTo("It works!");
}

View File

@ -43,10 +43,8 @@ public class ControllerTests {
public void test() throws Exception {
this.client.get().uri("/test")
.exchange()
.decodeEntity(String.class)
.assertThat()
.status().isOk()
.bodyEquals("It works!");
.expectStatus().isOk()
.expectBody(String.class).value().isEqualTo("It works!");
}

View File

@ -68,10 +68,8 @@ public class HttpServerTests {
public void test() throws Exception {
this.client.get().uri("/test")
.exchange()
.decodeEntity(String.class)
.assertThat()
.status().isOk()
.bodyEquals("It works!");
.expectStatus().isOk()
.expectBody(String.class).value().isEqualTo("It works!");
}
}

View File

@ -49,10 +49,8 @@ public class RouterFunctionTests {
public void test() throws Exception {
this.testClient.get().uri("/test")
.exchange()
.decodeEntity(String.class)
.assertThat()
.status().isOk()
.bodyEquals("It works!");
.expectStatus().isOk()
.expectBody(String.class).value().isEqualTo("It works!");
}
}