Refactor WebTestClient assertions

Rather than returning ExchangeActions that contains ExchangeInfo and
applies a chain of assertions, the exchange operation in WebTestClient
now has an extra step to decode the response.

The outcome of that is ExchangeResult<T>, parameterized with the
decoded body type, and containing the request and response details,
also providing access to built-inassertions via an assertThat()
instance method.

This approach lends itself better to decoding and asserting response
body types with generecis. It is also more friendly to using any
assertion library such as AssertJ since you get the result first
and then deal with assertions.
This commit is contained in:
Rossen Stoyanchev
2017-02-15 18:10:57 -05:00
parent 50d93d3794
commit 51f2042e97
24 changed files with 635 additions and 1548 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.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@@ -27,12 +28,16 @@ import java.util.function.Consumer;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
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.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;
@@ -41,6 +46,10 @@ 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.web.reactive.function.BodyExtractors.toDataBuffers;
import static org.springframework.web.reactive.function.BodyExtractors.toFlux;
import static org.springframework.web.reactive.function.BodyExtractors.toMono;
/**
* Default implementation of {@link WebTestClient}.
*
@@ -231,35 +240,91 @@ class DefaultWebTestClient implements WebTestClient {
return this;
}
@Override
public ExchangeActions exchange() {
return getExchangeActions(this.headerSpec.exchange());
public ResponseSpec exchange() {
return new DefaultResponseSpec(this.requestId, this.headerSpec.exchange());
}
@Override
public <T> ExchangeActions exchange(BodyInserter<T, ? super ClientHttpRequest> inserter) {
return getExchangeActions(this.headerSpec.exchange(inserter));
public <T> ResponseSpec exchange(BodyInserter<T, ? super ClientHttpRequest> inserter) {
return new DefaultResponseSpec(this.requestId, this.headerSpec.exchange(inserter));
}
@Override
public <T, S extends Publisher<T>> ExchangeActions exchange(S publisher, Class<T> elementClass) {
return getExchangeActions(this.headerSpec.exchange(publisher, elementClass));
public <T, S extends Publisher<T>> ResponseSpec exchange(S publisher, Class<T> elementClass) {
return new DefaultResponseSpec(this.requestId, this.headerSpec.exchange(publisher, elementClass));
}
}
private class DefaultResponseSpec implements ResponseSpec {
private final String requestId;
private final Mono<ClientResponse> responseMono;
public DefaultResponseSpec(String requestId, Mono<ClientResponse> responseMono) {
this.requestId = requestId;
this.responseMono = responseMono;
}
private ExchangeActions getExchangeActions(Mono<ClientResponse> responseMono) {
ClientResponse response = responseMono.block(getTimeout());
ExchangeInfo info = getExchangeInfo(response);
return new ExchangeActions(info);
@Override
public <T> ExchangeResult<T> decodeEntity(Class<T> entityClass) {
return decodeEntity(ResolvableType.forClass(entityClass));
}
private ExchangeInfo getExchangeInfo(ClientResponse clientResponse) {
WiretapConnector.Info wiretapInfo = connectorListener.retrieveRequest(this.requestId);
@Override
public <T> ExchangeResult<List<T>> decodeAndCollect(Class<T> elementClass) {
return decodeAndCollect(ResolvableType.forClass(elementClass));
}
@Override
public <T> ExchangeResult<Flux<T>> decodeFlux(Class<T> elementClass) {
return decodeFlux(ResolvableType.forClass(elementClass));
}
@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());
}
@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());
}
@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());
}
@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());
}
private <T> ExchangeResult<T> createTestExchange(T body, ClientResponse response) {
WiretapConnector.Info wiretapInfo = connectorListener.retrieveRequest(requestId);
ClientHttpRequest request = wiretapInfo.getRequest();
return new ExchangeInfo(request.getMethod(), request.getURI(), request.getHeaders(),
clientResponse, getTimeout());
return new ExchangeResult<T>(
request.getMethod(), request.getURI(), request.getHeaders(),
response.statusCode(), response.headers().asHttpHeaders(), body);
}
}
@@ -287,7 +352,7 @@ class DefaultWebTestClient implements WebTestClient {
public WiretapConnector.Info retrieveRequest(String requestId) {
WiretapConnector.Info info = this.exchanges.remove(requestId);
Assert.notNull(info, "No match for \"request-id\"=" + requestId);
Assert.notNull(info, "No match for request-id=" + requestId);
return info;
}
}

View File

@@ -1,98 +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.util.Collections;
import java.util.List;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
/**
* Entry point for applying assertions and actions on a performed exchange.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public final class ExchangeActions {
private final ExchangeInfo exchangeInfo;
public ExchangeActions(ExchangeInfo info) {
this.exchangeInfo = info;
}
/**
* Assert the status of the response.
* @return further options for asserting the status of the response
*/
public ResponseStatusAssertions assertStatus() {
return new ResponseStatusAssertions(this);
}
/**
* Assert specific, commonly used response headers.
* @return further options for asserting headers
*/
public ResponseHeadersAssertions assertHeaders() {
HttpHeaders headers = this.exchangeInfo.getResponse().headers().asHttpHeaders();
return new ResponseHeadersAssertions(this, headers);
}
/**
* Assert value(s) of the specified header name.
* @param headerName the header name
* @return options for asserting the header value(s)
*/
public ResponseHeaderAssertions assertHeader(String headerName) {
HttpHeaders headers = this.exchangeInfo.getResponse().headers().asHttpHeaders();
List<String> values = headers.getOrDefault(headerName, Collections.emptyList());
return new ResponseHeaderAssertions(this, headerName, values);
}
/**
* Assertions on the body of the response decoded as one or more response
* entities of the given type.
* @return options for asserting response entities
*/
public <T> ResponseEntityAssertions<T> assertEntity(Class<T> entityType) {
return new ResponseEntityAssertions<T>(this, ResolvableType.forClass(entityType));
}
/**
* Assertions on the body of the response.
*/
public ResponseBodyAssertions assertBody() {
return new ResponseBodyAssertions(this);
}
/**
* Log debug information about the exchange.
*/
public LoggingExchangeConsumer log() {
return new LoggingExchangeConsumer(this);
}
/**
* Return {@link ExchangeInfo} for direct access to request and response.
*/
public ExchangeInfo andReturn() {
return this.exchangeInfo;
}
}

View File

@@ -16,21 +16,28 @@
package org.springframework.test.web.reactive.server;
import java.net.URI;
import java.time.Duration;
import java.util.stream.Collectors;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.client.ClientResponse;
/**
* Contains information about a performed exchange.
* Container for request and response details including the decoded response
* body 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
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ExchangeInfo {
public class ExchangeResult<T> {
private final HttpMethod method;
@@ -38,33 +45,44 @@ public class ExchangeInfo {
private final HttpHeaders requestHeaders;
private final ClientResponse response;
private final HttpStatus status;
private final Duration responseTimeout;
private final HttpHeaders responseHeaders;
private final T responseBody;
public ExchangeInfo(HttpMethod httpMethod, URI uri, HttpHeaders requestHeaders,
ClientResponse response, Duration responseTimeout) {
public ExchangeResult(HttpMethod method, URI url, HttpHeaders requestHeaders,
HttpStatus status, HttpHeaders responseHeaders, T responseBody) {
this.method = httpMethod;
this.url = uri;
this.method = method;
this.url = url;
this.requestHeaders = requestHeaders;
this.response = response;
this.responseTimeout = responseTimeout;
this.status = status;
this.responseHeaders = responseHeaders;
this.responseBody = responseBody;
}
/**
* Return the HTTP method of the exchange.
* Provides access to built-in assertions on the response.
*/
public HttpMethod getHttpMethod() {
public ResponseAssertions<T> assertThat() {
return new ResponseAssertions<T>(this);
}
/**
* Return the request method of the exchange.
*/
public HttpMethod getRequestMethod() {
return this.method;
}
/**
* Return the URI of the exchange.
* Return the URL of the exchange.
*/
public URI getUrl() {
public URI getRequestUrl() {
return this.url;
}
@@ -76,30 +94,37 @@ public class ExchangeInfo {
}
/**
* Return the {@link ClientResponse} for the exchange.
* Return the response status.
*/
public ClientResponse getResponse() {
return this.response;
public HttpStatus getResponseStatus() {
return this.status;
}
/**
* Return the configured timeout for blocking on response data.
* Return the response headers.
*/
public Duration getResponseTimeout() {
return this.responseTimeout;
public HttpHeaders getResponseHeaders() {
return this.responseHeaders;
}
/**
* Return the decoded response body.
*/
public T getResponseBody() {
return this.responseBody;
}
@Override
public String toString() {
HttpStatus status = getResponse().statusCode();
HttpStatus status = this.status;
return "\n\n" +
formatValue("Request", getHttpMethod() + " " + getUrl()) +
formatValue("Request", this.method + " " + getRequestUrl()) +
formatValue("Status", status + " " + status.getReasonPhrase()) +
formatHeading("Response Headers") +
formatHeaders(getResponse().headers().asHttpHeaders()) +
formatHeaders(this.responseHeaders) +
formatHeading("Request Headers") +
formatHeaders(getRequestHeaders());
formatHeaders(this.requestHeaders);
}
private String formatHeading(String heading) {

View File

@@ -15,78 +15,88 @@
*/
package org.springframework.test.web.reactive.server;
import java.util.function.Consumer;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import org.springframework.http.CacheControl;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import static org.springframework.test.util.AssertionErrors.assertEquals;
import static org.springframework.test.util.AssertionErrors.assertTrue;
/**
* Assertions on specific, commonly used response headers.
* Provides methods for HTTP header assertions.
*
* @author Rossen Stoyanchev
* @since 5.0
* @see ResponseAssertions#header()
*/
public class ResponseHeadersAssertions {
public class HeaderAssertions<T> {
private final ExchangeActions exchangeActions;
private final ResponseAssertions<T> resultAssertions;
private final HttpHeaders headers;
ResponseHeadersAssertions(ExchangeActions actions, HttpHeaders headers) {
this.exchangeActions = actions;
public HeaderAssertions(HttpHeaders headers, ResponseAssertions<T> resultAssertions) {
this.resultAssertions = resultAssertions;
this.headers = headers;
}
public ExchangeActions cacheControl(CacheControl cacheControl) {
public ResponseAssertions<T> valueEquals(String headerName, String... values) {
List<String> actual = this.headers.get(headerName);
assertEquals("Response header [" + headerName + "]", Arrays.asList(values), actual);
return this.resultAssertions;
}
public ResponseAssertions<T> 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;
}
public ResponseAssertions<T> cacheControlEquals(CacheControl cacheControl) {
String actual = this.headers.getCacheControl();
assertEquals("Response header Cache-Control", cacheControl.getHeaderValue(), actual);
return this.exchangeActions;
return this.resultAssertions;
}
public ExchangeActions contentDisposition(ContentDisposition contentDisposition) {
public ResponseAssertions<T> contentDispositionEquals(ContentDisposition contentDisposition) {
ContentDisposition actual = this.headers.getContentDisposition();
assertEquals("Response header Content-Disposition", contentDisposition, actual);
return this.exchangeActions;
return this.resultAssertions;
}
public ExchangeActions contentLength(long contentLength) {
public ResponseAssertions<T> contentLengthEquals(long contentLength) {
long actual = this.headers.getContentLength();
assertEquals("Response header Content-Length", contentLength, actual);
return this.exchangeActions;
return this.resultAssertions;
}
public ExchangeActions contentType(MediaType mediaType) {
public ResponseAssertions<T> contentTypeEquals(MediaType mediaType) {
MediaType actual = this.headers.getContentType();
assertEquals("Response header Content-Type", mediaType, actual);
return this.exchangeActions;
return this.resultAssertions;
}
public ExchangeActions expires(int expires) {
public ResponseAssertions<T> expiresEquals(int expires) {
long actual = this.headers.getExpires();
assertEquals("Response header Expires", expires, actual);
return this.exchangeActions;
return this.resultAssertions;
}
public ExchangeActions lastModified(int lastModified) {
public ResponseAssertions<T> lastModifiedEquals(int lastModified) {
long actual = this.headers.getLastModified();
assertEquals("Response header Last-Modified", lastModified, actual);
return this.exchangeActions;
}
/**
* Custom assertions on the response headers with a {@link Consumer}.
* <p>Consider using statically imported methods to improve readability
* @param consumer consumer that will apply the custom action
*/
public ExchangeActions consume(Consumer<HttpHeaders> consumer) {
consumer.accept(this.headers);
return this.exchangeActions;
return this.resultAssertions;
}
}

View File

@@ -1,72 +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.util.Arrays;
import java.util.List;
import static org.springframework.test.util.AssertionErrors.assertEquals;
import static org.springframework.test.util.AssertionErrors.assertTrue;
/**
* Assertions on a List of values.
*
* @param <E> the type of element values in the collection
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ListAssertions<E> extends ObjectAssertions<List<E>, ListAssertions<E>> {
protected ListAssertions(ExchangeActions actions, List<E> collection, String errorPrefix) {
super(actions, collection, errorPrefix);
}
/**
* Assert the size of the list.
*/
public ListAssertions<E> hasSize(int size) {
assertEquals(getErrorPrefix() + " count", size, getValue().size());
return this;
}
/**
* Assert that the list contains all of the given values.
*/
@SuppressWarnings("unchecked")
public ListAssertions<E> contains(E... entities) {
Arrays.stream(entities).forEach(entity -> {
boolean result = getValue().contains(entity);
assertTrue(getErrorPrefix() + " do not contain " + entity, result);
});
return this;
}
/**
* Assert the list does not contain any of the given values.
*/
@SuppressWarnings("unchecked")
public ListAssertions<E> doesNotContain(E... entities) {
Arrays.stream(entities).forEach(entity -> {
boolean result = !getValue().contains(entity);
assertTrue(getErrorPrefix() + " should not contain " + entity, result);
});
return this;
}
}

View File

@@ -26,82 +26,85 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Provides options for logging information about the performed exchange.
* Provides options for logging diagnostic information about the exchange.
*
* @author Rossen Stoyanchev
* @since 5.0
* @see ResponseAssertions#log()
*/
public class LoggingExchangeConsumer {
public class LoggingActions<T> {
private static Log logger = LogFactory.getLog(LoggingExchangeConsumer.class);
private static Log logger = LogFactory.getLog(LoggingActions.class);
private final ResponseAssertions<T> resultAssertions;
private final ExchangeResult<T> exchange;
private final ExchangeActions exchangeActions;
public LoggingExchangeConsumer(ExchangeActions exchangeActions) {
this.exchangeActions = exchangeActions;
public LoggingActions(ExchangeResult<T> exchange, ResponseAssertions<T> resultAssertions) {
this.resultAssertions = resultAssertions;
this.exchange = exchange;
}
/**
* Log with {@link System#out}.
*/
public ExchangeActions toConsole() {
System.out.println(getOutput());
return this.exchangeActions;
public ResponseAssertions<T> toConsole() {
return toOutputStream(System.out);
}
/**
* Log with a given {@link OutputStream}.
*/
public ExchangeActions toOutputStream(OutputStream stream) {
public ResponseAssertions<T> toOutputStream(OutputStream stream) {
return toWriter(new PrintWriter(stream, true));
}
/**
* Log with a given {@link Writer}.
*/
public ExchangeActions toWriter(Writer 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.exchangeActions;
return this.resultAssertions;
}
/**
* Log if TRACE level logging is enabled.
*/
public ExchangeActions ifTraceEnabled() {
public ResponseAssertions<T> ifTraceEnabled() {
return doLog(Log::isTraceEnabled, Log::trace);
}
/**
* Log if DEBUG level logging is enabled.
*/
public ExchangeActions ifDebugEnabled() {
public ResponseAssertions<T> ifDebugEnabled() {
return doLog(Log::isDebugEnabled, Log::debug);
}
/**
* Log if INFO level logging is enabled.
*/
public ExchangeActions ifInfoEnabled() {
public ResponseAssertions<T> ifInfoEnabled() {
return doLog(Log::isInfoEnabled, Log::info);
}
private ExchangeActions doLog(Predicate<Log> logLevelPredicate, BiConsumer<Log, String> logAction) {
if (logLevelPredicate.test(logger)) {
logAction.accept(logger, getOutput());
private ResponseAssertions<T> doLog(Predicate<Log> predicate, BiConsumer<Log, String> consumer) {
if (predicate.test(logger)) {
consumer.accept(logger, getOutput());
}
return this.exchangeActions;
return this.resultAssertions;
}
private String getOutput() {
return this.exchangeActions.andReturn().toString();
return this.resultAssertions.toString();
}
}

View File

@@ -1,94 +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.util.Arrays;
import java.util.Map;
import static org.springframework.test.util.AssertionErrors.assertEquals;
import static org.springframework.test.util.AssertionErrors.assertTrue;
/**
* Assertions on a map of values.
*
* @param <K, V> the type of values in map
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class MapAssertions<K, V> extends ObjectAssertions<Map<K, V>, MapAssertions<K, V>> {
protected MapAssertions(ExchangeActions actions, Map<K, V> map, String errorPrefix) {
super(actions, map, errorPrefix);
}
/**
* Assert the size of the map.
*/
public MapAssertions<K, V> hasSize(int size) {
assertEquals(getErrorPrefix() + " count", size, getValue().size());
return this;
}
/**
* Assert that the map contains all of the given values.
*/
@SuppressWarnings("unchecked")
public MapAssertions<K, V> contains(K key, V value) {
V actual = getValue().get(key);
assertEquals(getErrorPrefix() + " do not contain " + value, value, actual);
return this;
}
/**
* Assert that the map contains all of the given keys.
*/
@SuppressWarnings("unchecked")
public MapAssertions<K, V> containsKeys(K... keys) {
Arrays.stream(keys).forEach(key -> {
boolean result = getValue().containsKey(key);
assertTrue(getErrorPrefix() + " does not contain key " + key, result);
});
return this;
}
/**
* Assert that the map contains all of the given keys.
*/
@SuppressWarnings("unchecked")
public MapAssertions<K, V> containsValues(V... values) {
Arrays.stream(values).forEach(value -> {
boolean result = getValue().containsValue(value);
assertTrue(getErrorPrefix() + " does not contain value " + value, result);
});
return this;
}
/**
* Assert the map does not contain any of the given keys.
*/
@SuppressWarnings("unchecked")
public MapAssertions<K, V> doesNotContain(K... keys) {
Arrays.stream(keys).forEach(key -> {
boolean result = !getValue().containsKey(key);
assertTrue(getErrorPrefix() + " should not contain " + key, result);
});
return this;
}
}

View File

@@ -1,107 +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.util.Arrays;
import java.util.List;
import org.springframework.util.MultiValueMap;
import static org.springframework.test.util.AssertionErrors.assertEquals;
import static org.springframework.test.util.AssertionErrors.assertTrue;
import static org.springframework.test.util.AssertionErrors.fail;
/**
* Assertions on the values of a {@link MultiValueMap} entry.
*
* @param <V> the type of values in the map.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class MultiValueMapEntryAssertions<V> {
private final ExchangeActions exchangeActions;
private final String name;
private final MultiValueMap<String, V> map;
private final String errorMessagePrefix;
MultiValueMapEntryAssertions(ExchangeActions actions, String name,
MultiValueMap<String, V> map, String errorMessagePrefix) {
this.exchangeActions = actions;
this.name = name;
this.map = map;
this.errorMessagePrefix = errorMessagePrefix + " " + this.name;
}
/**
* The given values are equal to the actual values.
* @param values the values to match
*/
@SuppressWarnings("unchecked")
public ExchangeActions isEqualTo(V... values) {
List<V> actual = this.map.get(this.name);
assertEquals(this.errorMessagePrefix, Arrays.asList(values), actual);
return this.exchangeActions;
}
/**
* The list of actual values contains the given values.
* @param values the values to match
*/
@SuppressWarnings("unchecked")
public ExchangeActions hasValues(V... values) {
List<V> actual = this.map.get(this.name);
List<V> expected = Arrays.asList(values);
String message = getErrorMessagePrefix() + " does not contain " + expected;
assertTrue(message, actual.containsAll(expected));
return this.exchangeActions;
}
// Protected methods for sub-classes
protected ExchangeActions getExchangeActions() {
return this.exchangeActions;
}
protected String getName() {
return this.name;
}
protected MultiValueMap<String, V> getMap() {
return this.map;
}
protected String getErrorMessagePrefix() {
return this.errorMessagePrefix;
}
protected V getValue(int index) {
List<V> actualValues = getMap().get(getName());
if (actualValues == null || index >= actualValues.size()) {
fail(getErrorMessagePrefix() + " does not have values at index[" + index + "]: " + actualValues);
}
return actualValues.get(index);
}
}

View File

@@ -1,129 +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.time.Duration;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.springframework.web.reactive.function.client.ClientResponse;
import static org.springframework.test.util.AssertionErrors.assertEquals;
/**
* Assertions on an Object.
*
* @param <V> the type of Object to apply assertions to
* @param <S> assertion method return type
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ObjectAssertions<V, S extends ObjectAssertions<V, S>> {
private final ExchangeActions exchangeActions;
private V value;
private final Supplier<V> valueSupplier;
private final String errorPrefix;
protected ObjectAssertions(ExchangeActions exchangeActions, V value, String errorPrefix) {
this(exchangeActions, () -> value, errorPrefix);
}
protected ObjectAssertions(ExchangeActions exchangeActions, Supplier<V> valueSupplier, String errorPrefix) {
this.exchangeActions = exchangeActions;
this.valueSupplier = valueSupplier;
this.errorPrefix = errorPrefix;
}
/**
* Assert the actual value is equal to the given expected value.
*/
public <T extends S> T isEqualTo(V expected) {
assertEquals(this.errorPrefix, expected, getValue());
return self();
}
/**
* Assert the actual value is not equal to the expected value.
*/
public <T extends S> T isNotEqualTo(V expected) {
assertEquals(this.errorPrefix, expected, getValue());
return self();
}
/**
* Custom assertions on the Object value with a {@link Consumer}.
* <p>Consider using statically imported methods to improve readability
* @param consumer consumer that will apply the custom action
*/
public ExchangeActions consume(Consumer<V> consumer) {
consumer.accept(this.getValue());
return this.exchangeActions;
}
/**
* Continue with more assertions or actions on the response.
*/
public ExchangeActions and() {
return this.exchangeActions;
}
/**
* Return {@link ExchangeInfo} for direct access to request and response.
*/
public ExchangeInfo andReturn() {
return this.exchangeActions.andReturn();
}
// Protected methods for sub-classes
protected ExchangeActions getExchangeActions() {
return this.exchangeActions;
}
protected ClientResponse getResponse() {
return this.exchangeActions.andReturn().getResponse();
}
protected Duration getTimeout() {
return this.exchangeActions.andReturn().getResponseTimeout();
}
protected String getErrorPrefix() {
return this.errorPrefix;
}
protected V getValue() {
if (this.value == null) {
this.value = this.valueSupplier.get();
}
return this.value;
}
@SuppressWarnings("unchecked")
protected <T extends S> T self() {
return (T) this;
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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

@@ -1,75 +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.nio.ByteBuffer;
import java.time.Duration;
import java.util.Map;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.ResolvableType;
import org.springframework.web.reactive.function.client.ClientResponse;
import static org.springframework.web.reactive.function.BodyExtractors.toMono;
/**
* Assertions on the body of the response.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ResponseBodyAssertions {
private final ExchangeActions exchangeActions;
public ResponseBodyAssertions(ExchangeActions exchangeActions) {
this.exchangeActions = exchangeActions;
}
/**
* Assert the response does not have any content.
*/
public ExchangeActions isEmpty() {
Flux<?> body = getResponse().bodyToFlux(ByteBuffer.class);
StepVerifier.create(body).expectComplete().verify(getTimeout());
return this.exchangeActions;
}
/**
* Assert the response decoded as a Map of String key-value pairs.
*/
public MapAssertions<String, String> asMap() {
ResolvableType type = ResolvableType.forClassWithGenerics(Map.class, String.class, String.class);
Mono<Map<String, String>> mono = getResponse().body(toMono(type));
Map<String, String> map = mono.block(getTimeout());
return new MapAssertions<>(this.exchangeActions, map, "Response body map");
}
private ClientResponse getResponse() {
return this.exchangeActions.andReturn().getResponse();
}
private Duration getTimeout() {
return this.exchangeActions.andReturn().getResponseTimeout();
}
}

View File

@@ -1,83 +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.util.List;
import java.util.Map;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.ResolvableType;
import static org.springframework.web.reactive.function.BodyExtractors.toFlux;
import static org.springframework.web.reactive.function.BodyExtractors.toMono;
/**
* Assertions on the response body decoded as a single entity.
*
* @param <T> the response entity type
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ResponseEntityAssertions<T> extends ObjectAssertions<T, ResponseEntityAssertions<T>> {
private final ResolvableType entityType;
ResponseEntityAssertions(ExchangeActions actions, ResolvableType entityType) {
super(actions, () -> initEntity(actions, entityType), "Response body");
this.entityType = entityType;
}
private static <T> T initEntity(ExchangeActions exchangeActions, ResolvableType entityType) {
ExchangeInfo info = exchangeActions.andReturn();
Mono<T> mono = info.getResponse().body(toMono(entityType));
return mono.block(info.getResponseTimeout());
}
/**
* Assert the response decoded as a List of entities of the given type.
*/
public ListAssertions<T> list() {
Flux<T> flux = getResponse().body(toFlux(this.entityType));
List<T> list = flux.collectList().block(getTimeout());
return new ListAssertions<T>(getExchangeActions(), list, "Response entity collection");
}
/**
* Assert the response decoded as a Map of entities with String keys.
*/
public MapAssertions<String, T> map() {
ResolvableType keyType = ResolvableType.forClass(String.class);
ResolvableType type = ResolvableType.forClassWithGenerics(Map.class, keyType, this.entityType);
Mono<Map<String, T>> mono = getResponse().body(toMono(type));
Map<String, T> map = mono.block(getTimeout());
return new MapAssertions<>(getExchangeActions(), map, "Response entity map");
}
/**
* Assert the response content using a {@link StepVerifier}.
*/
public StepVerifier.FirstStep<T> stepVerifier() {
Flux<T> flux = getResponse().body(toFlux(this.entityType));
return StepVerifier.create(flux);
}
}

View File

@@ -1,47 +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.util.List;
import org.springframework.util.CollectionUtils;
/**
* Assertions on the values of a specific header.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ResponseHeaderAssertions extends StringAssertions {
private final List<String> values;
ResponseHeaderAssertions(ExchangeActions actions, String headerName, List<String> values) {
super(actions, initHeaderValue(values), "Response header [" + headerName + "]");
this.values = values;
}
private static String initHeaderValue(List<String> values) {
return CollectionUtils.isEmpty(values) ? null : values.get(0);
}
public ListAssertions<String> values() {
return new ListAssertions<>(getExchangeActions(), this.values, getErrorPrefix());
}
}

View File

@@ -1,606 +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.util.function.Consumer;
import org.springframework.http.HttpStatus;
import static org.springframework.test.util.AssertionErrors.assertEquals;
/**
* Provides methods for asserting the response status.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
@SuppressWarnings("unused")
public class ResponseStatusAssertions {
private final ExchangeActions exchangeActions;
private final HttpStatus httpStatus;
ResponseStatusAssertions(ExchangeActions actions) {
this.exchangeActions = actions;
this.httpStatus = actions.andReturn().getResponse().statusCode();
}
public ExchangeActions is(int status) {
assertEquals("Response status", status, this.httpStatus.value());
return this.exchangeActions;
}
/**
* Assert the response status code is in the 1xx range.
*/
public ExchangeActions is1xxInformational() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.INFORMATIONAL, this.httpStatus.series());
return this.exchangeActions;
}
/**
* Assert the response status code is in the 2xx range.
*/
public ExchangeActions is2xxSuccessful() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.SUCCESSFUL, this.httpStatus.series());
return this.exchangeActions;
}
/**
* Assert the response status code is in the 3xx range.
*/
public ExchangeActions is3xxRedirection() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.REDIRECTION, this.httpStatus.series());
return this.exchangeActions;
}
/**
* Assert the response status code is in the 4xx range.
*/
public ExchangeActions is4xxClientError() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.CLIENT_ERROR, this.httpStatus.series());
return this.exchangeActions;
}
/**
* Assert the response status code is in the 5xx range.
*/
public ExchangeActions is5xxServerError() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.SERVER_ERROR, this.httpStatus.series());
return this.exchangeActions;
}
/**
* Custom assertions on the response status with a {@link Consumer}.
* <p>Consider using statically imported methods to improve readability
* @param consumer consumer that will apply the custom action
*/
public ExchangeActions consume(Consumer<HttpStatus> consumer) {
consumer.accept(this.httpStatus);
return this.exchangeActions;
}
/**
* Assert the response error message.
*/
public ExchangeActions reason(String reason) {
assertEquals("Response status reason", reason, this.httpStatus.getReasonPhrase());
return this.exchangeActions;
}
/**
* Assert the response status code is {@code HttpStatus.CONTINUE} (100).
*/
public ExchangeActions isContinue() {
return assertStatusIsEqualTo(HttpStatus.CONTINUE);
}
/**
* Assert the response status code is {@code HttpStatus.SWITCHING_PROTOCOLS} (101).
*/
public ExchangeActions isSwitchingProtocols() {
return assertStatusIsEqualTo(HttpStatus.SWITCHING_PROTOCOLS);
}
/**
* Assert the response status code is {@code HttpStatus.PROCESSING} (102).
*/
public ExchangeActions isProcessing() {
return assertStatusIsEqualTo(HttpStatus.PROCESSING);
}
/**
* Assert the response status code is {@code HttpStatus.CHECKPOINT} (103).
*/
public ExchangeActions isCheckpoint() {
return assertStatusIsEqualTo(HttpStatus.valueOf(103));
}
/**
* Assert the response status code is {@code HttpStatus.OK} (200).
*/
public ExchangeActions isOk() {
return assertStatusIsEqualTo(HttpStatus.OK);
}
/**
* Assert the response status code is {@code HttpStatus.CREATED} (201).
*/
public ExchangeActions isCreated() {
return assertStatusIsEqualTo(HttpStatus.CREATED);
}
/**
* Assert the response status code is {@code HttpStatus.ACCEPTED} (202).
*/
public ExchangeActions isAccepted() {
return assertStatusIsEqualTo(HttpStatus.ACCEPTED);
}
/**
* Assert the response status code is {@code HttpStatus.NON_AUTHORITATIVE_INFORMATION} (203).
*/
public ExchangeActions isNonAuthoritativeInformation() {
return assertStatusIsEqualTo(HttpStatus.NON_AUTHORITATIVE_INFORMATION);
}
/**
* Assert the response status code is {@code HttpStatus.NO_CONTENT} (204).
*/
public ExchangeActions isNoContent() {
return assertStatusIsEqualTo(HttpStatus.NO_CONTENT);
}
/**
* Assert the response status code is {@code HttpStatus.RESET_CONTENT} (205).
*/
public ExchangeActions isResetContent() {
return assertStatusIsEqualTo(HttpStatus.RESET_CONTENT);
}
/**
* Assert the response status code is {@code HttpStatus.PARTIAL_CONTENT} (206).
*/
public ExchangeActions isPartialContent() {
return assertStatusIsEqualTo(HttpStatus.PARTIAL_CONTENT);
}
/**
* Assert the response status code is {@code HttpStatus.MULTI_STATUS} (207).
*/
public ExchangeActions isMultiStatus() {
return assertStatusIsEqualTo(HttpStatus.MULTI_STATUS);
}
/**
* Assert the response status code is {@code HttpStatus.ALREADY_REPORTED} (208).
*/
public ExchangeActions isAlreadyReported() {
return assertStatusIsEqualTo(HttpStatus.ALREADY_REPORTED);
}
/**
* Assert the response status code is {@code HttpStatus.IM_USED} (226).
*/
public ExchangeActions isImUsed() {
return assertStatusIsEqualTo(HttpStatus.IM_USED);
}
/**
* Assert the response status code is {@code HttpStatus.MULTIPLE_CHOICES} (300).
*/
public ExchangeActions isMultipleChoices() {
return assertStatusIsEqualTo(HttpStatus.MULTIPLE_CHOICES);
}
/**
* Assert the response status code is {@code HttpStatus.MOVED_PERMANENTLY} (301).
*/
public ExchangeActions isMovedPermanently() {
return assertStatusIsEqualTo(HttpStatus.MOVED_PERMANENTLY);
}
/**
* Assert the response status code is {@code HttpStatus.FOUND} (302).
*/
public ExchangeActions isFound() {
return assertStatusIsEqualTo(HttpStatus.FOUND);
}
/**
* Assert the response status code is {@code HttpStatus.MOVED_TEMPORARILY} (302).
* @see #isFound()
* @deprecated in favor of {@link #isFound()}
*/
@Deprecated
public ExchangeActions isMovedTemporarily() {
return assertStatusIsEqualTo(HttpStatus.MOVED_TEMPORARILY);
}
/**
* Assert the response status code is {@code HttpStatus.SEE_OTHER} (303).
*/
public ExchangeActions isSeeOther() {
return assertStatusIsEqualTo(HttpStatus.SEE_OTHER);
}
/**
* Assert the response status code is {@code HttpStatus.NOT_MODIFIED} (304).
*/
public ExchangeActions isNotModified() {
return assertStatusIsEqualTo(HttpStatus.NOT_MODIFIED);
}
/**
* Assert the response status code is {@code HttpStatus.USE_PROXY} (305).
* @deprecated matching the deprecation of {@code HttpStatus.USE_PROXY}
*/
@Deprecated
public ExchangeActions isUseProxy() {
return assertStatusIsEqualTo(HttpStatus.USE_PROXY);
}
/**
* Assert the response status code is {@code HttpStatus.TEMPORARY_REDIRECT} (307).
*/
public ExchangeActions isTemporaryRedirect() {
return assertStatusIsEqualTo(HttpStatus.TEMPORARY_REDIRECT);
}
/**
* Assert the response status code is {@code HttpStatus.PERMANENT_REDIRECT} (308).
*/
public ExchangeActions isPermanentRedirect() {
return assertStatusIsEqualTo(HttpStatus.valueOf(308));
}
/**
* Assert the response status code is {@code HttpStatus.BAD_REQUEST} (400).
*/
public ExchangeActions isBadRequest() {
return assertStatusIsEqualTo(HttpStatus.BAD_REQUEST);
}
/**
* Assert the response status code is {@code HttpStatus.UNAUTHORIZED} (401).
*/
public ExchangeActions isUnauthorized() {
return assertStatusIsEqualTo(HttpStatus.UNAUTHORIZED);
}
/**
* Assert the response status code is {@code HttpStatus.PAYMENT_REQUIRED} (402).
*/
public ExchangeActions isPaymentRequired() {
return assertStatusIsEqualTo(HttpStatus.PAYMENT_REQUIRED);
}
/**
* Assert the response status code is {@code HttpStatus.FORBIDDEN} (403).
*/
public ExchangeActions isForbidden() {
return assertStatusIsEqualTo(HttpStatus.FORBIDDEN);
}
/**
* Assert the response status code is {@code HttpStatus.NOT_FOUND} (404).
*/
public ExchangeActions isNotFound() {
return assertStatusIsEqualTo(HttpStatus.NOT_FOUND);
}
/**
* Assert the response status code is {@code HttpStatus.METHOD_NOT_ALLOWED} (405).
*/
public ExchangeActions isMethodNotAllowed() {
return assertStatusIsEqualTo(HttpStatus.METHOD_NOT_ALLOWED);
}
/**
* Assert the response status code is {@code HttpStatus.NOT_ACCEPTABLE} (406).
*/
public ExchangeActions isNotAcceptable() {
return assertStatusIsEqualTo(HttpStatus.NOT_ACCEPTABLE);
}
/**
* Assert the response status code is {@code HttpStatus.PROXY_AUTHENTICATION_REQUIRED} (407).
*/
public ExchangeActions isProxyAuthenticationRequired() {
return assertStatusIsEqualTo(HttpStatus.PROXY_AUTHENTICATION_REQUIRED);
}
/**
* Assert the response status code is {@code HttpStatus.REQUEST_TIMEOUT} (408).
*/
public ExchangeActions isRequestTimeout() {
return assertStatusIsEqualTo(HttpStatus.REQUEST_TIMEOUT);
}
/**
* Assert the response status code is {@code HttpStatus.CONFLICT} (409).
*/
public ExchangeActions isConflict() {
return assertStatusIsEqualTo(HttpStatus.CONFLICT);
}
/**
* Assert the response status code is {@code HttpStatus.GONE} (410).
*/
public ExchangeActions isGone() {
return assertStatusIsEqualTo(HttpStatus.GONE);
}
/**
* Assert the response status code is {@code HttpStatus.LENGTH_REQUIRED} (411).
*/
public ExchangeActions isLengthRequired() {
return assertStatusIsEqualTo(HttpStatus.LENGTH_REQUIRED);
}
/**
* Assert the response status code is {@code HttpStatus.PRECONDITION_FAILED} (412).
*/
public ExchangeActions isPreconditionFailed() {
return assertStatusIsEqualTo(HttpStatus.PRECONDITION_FAILED);
}
/**
* Assert the response status code is {@code HttpStatus.PAYLOAD_TOO_LARGE} (413).
* @since 4.1
*/
public ExchangeActions isPayloadTooLarge() {
return assertStatusIsEqualTo(HttpStatus.PAYLOAD_TOO_LARGE);
}
/**
* Assert the response status code is {@code HttpStatus.REQUEST_ENTITY_TOO_LARGE} (413).
* @deprecated matching the deprecation of {@code HttpStatus.REQUEST_ENTITY_TOO_LARGE}
* @see #isPayloadTooLarge()
*/
@Deprecated
public ExchangeActions isRequestEntityTooLarge() {
return assertStatusIsEqualTo(HttpStatus.REQUEST_ENTITY_TOO_LARGE);
}
/**
* Assert the response status code is {@code HttpStatus.REQUEST_URI_TOO_LONG} (414).
* @since 4.1
*/
public ExchangeActions isUriTooLong() {
return assertStatusIsEqualTo(HttpStatus.URI_TOO_LONG);
}
/**
* Assert the response status code is {@code HttpStatus.REQUEST_URI_TOO_LONG} (414).
* @deprecated matching the deprecation of {@code HttpStatus.REQUEST_URI_TOO_LONG}
* @see #isUriTooLong()
*/
@Deprecated
public ExchangeActions isRequestUriTooLong() {
return assertStatusIsEqualTo(HttpStatus.REQUEST_URI_TOO_LONG);
}
/**
* Assert the response status code is {@code HttpStatus.UNSUPPORTED_MEDIA_TYPE} (415).
*/
public ExchangeActions isUnsupportedMediaType() {
return assertStatusIsEqualTo(HttpStatus.UNSUPPORTED_MEDIA_TYPE);
}
/**
* Assert the response status code is {@code HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE} (416).
*/
public ExchangeActions isRequestedRangeNotSatisfiable() {
return assertStatusIsEqualTo(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
}
/**
* Assert the response status code is {@code HttpStatus.EXPECTATION_FAILED} (417).
*/
public ExchangeActions isExpectationFailed() {
return assertStatusIsEqualTo(HttpStatus.EXPECTATION_FAILED);
}
/**
* Assert the response status code is {@code HttpStatus.I_AM_A_TEAPOT} (418).
*/
public ExchangeActions isIAmATeapot() {
return assertStatusIsEqualTo(HttpStatus.valueOf(418));
}
/**
* Assert the response status code is {@code HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE} (419).
* @deprecated matching the deprecation of {@code HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE}
*/
@Deprecated
public ExchangeActions isInsufficientSpaceOnResource() {
return assertStatusIsEqualTo(HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE);
}
/**
* Assert the response status code is {@code HttpStatus.METHOD_FAILURE} (420).
* @deprecated matching the deprecation of {@code HttpStatus.METHOD_FAILURE}
*/
@Deprecated
public ExchangeActions isMethodFailure() {
return assertStatusIsEqualTo(HttpStatus.METHOD_FAILURE);
}
/**
* Assert the response status code is {@code HttpStatus.DESTINATION_LOCKED} (421).
* @deprecated matching the deprecation of {@code HttpStatus.DESTINATION_LOCKED}
*/
@Deprecated
public ExchangeActions isDestinationLocked() {
return assertStatusIsEqualTo(HttpStatus.DESTINATION_LOCKED);
}
/**
* Assert the response status code is {@code HttpStatus.UNPROCESSABLE_ENTITY} (422).
*/
public ExchangeActions isUnprocessableEntity() {
return assertStatusIsEqualTo(HttpStatus.UNPROCESSABLE_ENTITY);
}
/**
* Assert the response status code is {@code HttpStatus.LOCKED} (423).
*/
public ExchangeActions isLocked() {
return assertStatusIsEqualTo(HttpStatus.LOCKED);
}
/**
* Assert the response status code is {@code HttpStatus.FAILED_DEPENDENCY} (424).
*/
public ExchangeActions isFailedDependency() {
return assertStatusIsEqualTo(HttpStatus.FAILED_DEPENDENCY);
}
/**
* Assert the response status code is {@code HttpStatus.UPGRADE_REQUIRED} (426).
*/
public ExchangeActions isUpgradeRequired() {
return assertStatusIsEqualTo(HttpStatus.UPGRADE_REQUIRED);
}
/**
* Assert the response status code is {@code HttpStatus.PRECONDITION_REQUIRED} (428).
*/
public ExchangeActions isPreconditionRequired() {
return assertStatusIsEqualTo(HttpStatus.valueOf(428));
}
/**
* Assert the response status code is {@code HttpStatus.TOO_MANY_REQUESTS} (429).
*/
public ExchangeActions isTooManyRequests() {
return assertStatusIsEqualTo(HttpStatus.valueOf(429));
}
/**
* Assert the response status code is {@code HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE} (431).
*/
public ExchangeActions isRequestHeaderFieldsTooLarge() {
return assertStatusIsEqualTo(HttpStatus.valueOf(431));
}
/**
* Assert the response status code is {@code HttpStatus.UNAVAILABLE_FOR_LEGAL_REASONS} (451).
* @since 4.3
*/
public ExchangeActions isUnavailableForLegalReasons() {
return assertStatusIsEqualTo(HttpStatus.valueOf(451));
}
/**
* Assert the response status code is {@code HttpStatus.INTERNAL_SERVER_ERROR} (500).
*/
public ExchangeActions isInternalServerError() {
return assertStatusIsEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* Assert the response status code is {@code HttpStatus.NOT_IMPLEMENTED} (501).
*/
public ExchangeActions isNotImplemented() {
return assertStatusIsEqualTo(HttpStatus.NOT_IMPLEMENTED);
}
/**
* Assert the response status code is {@code HttpStatus.BAD_GATEWAY} (502).
*/
public ExchangeActions isBadGateway() {
return assertStatusIsEqualTo(HttpStatus.BAD_GATEWAY);
}
/**
* Assert the response status code is {@code HttpStatus.SERVICE_UNAVAILABLE} (503).
*/
public ExchangeActions isServiceUnavailable() {
return assertStatusIsEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
}
/**
* Assert the response status code is {@code HttpStatus.GATEWAY_TIMEOUT} (504).
*/
public ExchangeActions isGatewayTimeout() {
return assertStatusIsEqualTo(HttpStatus.GATEWAY_TIMEOUT);
}
/**
* Assert the response status code is {@code HttpStatus.HTTP_VERSION_NOT_SUPPORTED} (505).
*/
public ExchangeActions isHttpVersionNotSupported() {
return assertStatusIsEqualTo(HttpStatus.HTTP_VERSION_NOT_SUPPORTED);
}
/**
* Assert the response status code is {@code HttpStatus.VARIANT_ALSO_NEGOTIATES} (506).
*/
public ExchangeActions isVariantAlsoNegotiates() {
return assertStatusIsEqualTo(HttpStatus.VARIANT_ALSO_NEGOTIATES);
}
/**
* Assert the response status code is {@code HttpStatus.INSUFFICIENT_STORAGE} (507).
*/
public ExchangeActions isInsufficientStorage() {
return assertStatusIsEqualTo(HttpStatus.INSUFFICIENT_STORAGE);
}
/**
* Assert the response status code is {@code HttpStatus.LOOP_DETECTED} (508).
*/
public ExchangeActions isLoopDetected() {
return assertStatusIsEqualTo(HttpStatus.LOOP_DETECTED);
}
/**
* Assert the response status code is {@code HttpStatus.BANDWIDTH_LIMIT_EXCEEDED} (509).
*/
public ExchangeActions isBandwidthLimitExceeded() {
return assertStatusIsEqualTo(HttpStatus.valueOf(509));
}
/**
* Assert the response status code is {@code HttpStatus.NOT_EXTENDED} (510).
*/
public ExchangeActions isNotExtended() {
return assertStatusIsEqualTo(HttpStatus.NOT_EXTENDED);
}
/**
* Assert the response status code is {@code HttpStatus.NETWORK_AUTHENTICATION_REQUIRED} (511).
*/
public ExchangeActions isNetworkAuthenticationRequired() {
return assertStatusIsEqualTo(HttpStatus.valueOf(511));
}
private ExchangeActions assertStatusIsEqualTo(final HttpStatus status) {
assertEquals("Status", status, this.httpStatus);
return this.exchangeActions;
}
}

View File

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

View File

@@ -1,72 +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.util.Arrays;
import java.util.regex.Pattern;
import static org.springframework.test.util.AssertionErrors.assertTrue;
/**
* Assertions on a String value.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class StringAssertions extends ObjectAssertions<String, StringAssertions> {
public StringAssertions(ExchangeActions exchangeActions, String value, String errorPrefix) {
super(exchangeActions, value, errorPrefix);
}
/**
* The value {@link String#contains contains} the given sub-strings.
* @param values the values to match
*/
public StringAssertions contains(CharSequence... values) {
Arrays.stream(values).forEach(value -> {
String message = getErrorPrefix() + " does not contain " + value;
assertTrue(message, getValue().contains(value));
});
return this;
}
/**
* The value does not {@link String#contains contain} the given sub-strings.
* @param values the values to match
*/
public StringAssertions doesNotContain(CharSequence... values) {
Arrays.stream(values).forEach(value -> {
String message = getErrorPrefix() + " contains " + value + " but shouldn't";
assertTrue(message, !getValue().contains(value));
});
return this;
}
/**
* The value matches the given regex pattern value.
* @param pattern the values to be compiled with {@link Pattern}
*/
public StringAssertions matches(String pattern) {
boolean match = Pattern.compile(pattern).matcher(getValue()).matches();
String message = getErrorPrefix() + " with value " + getValue() + " does not match " + pattern;
assertTrue(message, match);
return this;
}
}

View File

@@ -25,8 +25,10 @@ import java.util.function.Consumer;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ResolvableType;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@@ -330,12 +332,13 @@ public interface WebTestClient {
/**
* Contract for specifying the URI for a request.
* Specification for providing the URI of a request.
*/
interface UriSpec {
/**
* Specify the URI using an absolute, fully constructed {@link URI}.
* @return spec to add headers or perform the exchange
*/
HeaderSpec uri(URI uri);
@@ -343,6 +346,7 @@ public interface WebTestClient {
* Specify the URI for the request using a URI template and URI variables.
* If a {@link UriBuilderFactory} was configured for the client (e.g.
* with a base URI) it will be used to expand the URI template.
* @return spec to add headers or perform the exchange
*/
HeaderSpec uri(String uri, Object... uriVariables);
@@ -350,19 +354,21 @@ public interface WebTestClient {
* Specify the URI for the request using a URI template and URI variables.
* If a {@link UriBuilderFactory} was configured for the client (e.g.
* with a base URI) it will be used to expand the URI template.
* @return spec to add headers or perform the exchange
*/
HeaderSpec uri(String uri, Map<String, ?> uriVariables);
/**
* Build the URI for the request with a {@link UriBuilder} obtained
* through the {@link UriBuilderFactory} configured for this client.
* @return spec to add headers or perform the exchange
*/
HeaderSpec uri(Function<UriBuilder, URI> uriFunction);
}
/**
* Contract for specifying request headers leading up to the exchange.
* Specification for adding request headers and performing an exchange.
*/
interface HeaderSpec {
@@ -370,7 +376,7 @@ public interface WebTestClient {
* Set the list of acceptable {@linkplain MediaType media types}, as
* specified by the {@code Accept} header.
* @param acceptableMediaTypes the acceptable media types
* @return this builder
* @return the same instance
*/
HeaderSpec accept(MediaType... acceptableMediaTypes);
@@ -378,7 +384,7 @@ public interface WebTestClient {
* Set the list of acceptable {@linkplain Charset charsets}, as specified
* by the {@code Accept-Charset} header.
* @param acceptableCharsets the acceptable charsets
* @return this builder
* @return the same instance
*/
HeaderSpec acceptCharset(Charset... acceptableCharsets);
@@ -386,7 +392,7 @@ public interface WebTestClient {
* Set the length of the body in bytes, as specified by the
* {@code Content-Length} header.
* @param contentLength the content length
* @return this builder
* @return the same instance
* @see HttpHeaders#setContentLength(long)
*/
HeaderSpec contentLength(long contentLength);
@@ -395,7 +401,7 @@ public interface WebTestClient {
* Set the {@linkplain MediaType media type} of the body, as specified
* by the {@code Content-Type} header.
* @param contentType the content type
* @return this builder
* @return the same instance
* @see HttpHeaders#setContentType(MediaType)
*/
HeaderSpec contentType(MediaType contentType);
@@ -404,7 +410,7 @@ public interface WebTestClient {
* Add a cookie with the given name and value.
* @param name the cookie name
* @param value the cookie value
* @return this builder
* @return the same instance
*/
HeaderSpec cookie(String name, String value);
@@ -412,7 +418,7 @@ public interface WebTestClient {
* Copy the given cookies into the entity's cookies map.
*
* @param cookies the existing cookies to copy from
* @return this builder
* @return the same instance
*/
HeaderSpec cookies(MultiValueMap<String, String> cookies);
@@ -421,14 +427,14 @@ public interface WebTestClient {
* <p>The date should be specified as the number of milliseconds since
* January 1, 1970 GMT.
* @param ifModifiedSince the new value of the header
* @return this builder
* @return the same instance
*/
HeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince);
/**
* Set the values of the {@code If-None-Match} header.
* @param ifNoneMatches the new value of the header
* @return this builder
* @return the same instance
*/
HeaderSpec ifNoneMatch(String... ifNoneMatches);
@@ -436,42 +442,112 @@ public interface WebTestClient {
* Add the given, single header value under the given name.
* @param headerName the header name
* @param headerValues the header value(s)
* @return this builder
* @return the same instance
*/
HeaderSpec header(String headerName, String... headerValues);
/**
* Copy the given headers into the entity's headers map.
* @param headers the existing headers to copy from
* @return this builder
* @return the same instance
*/
HeaderSpec headers(HttpHeaders headers);
/**
* Perform the request without a request body.
* @return options for asserting the response with
* Perform the exchange without a request body.
* @return spec for decoding the response
*/
ExchangeActions exchange();
ResponseSpec exchange();
/**
* Set the body of the request to the given {@code BodyInserter} and
* perform the request.
* @param inserter the {@code BodyInserter} that writes to the request
* @param <T> the type contained in the body
* @return options for asserting the response with
* Perform the exchange with the body for the request populated using
* a {@link BodyInserter}.
* @param inserter the inserter
* @param <T> the body type, or the the element type (for a stream)
* @return spec for decoding the response
* @see org.springframework.web.reactive.function.BodyInserters
*/
<T> ExchangeActions exchange(BodyInserter<T, ? super ClientHttpRequest> inserter);
<T> ResponseSpec exchange(BodyInserter<T, ? super ClientHttpRequest> inserter);
/**
* Set the body of the request to the given {@code Publisher} and
* perform the request.
* @param publisher the {@code Publisher} to write to the request
* Perform the exchange and use the given {@code Publisher} for the
* request body.
* @param publisher the request body data
* @param elementClass the class of elements contained in the publisher
* @param <T> the type of the elements contained in the publisher
* @param <S> the type of the {@code Publisher}
* @return options for asserting the response with
* @return spec for decoding the response
*/
<T, S extends Publisher<T>> ExchangeActions exchange(S publisher, Class<T> elementClass);
<T, S extends Publisher<T>> ResponseSpec exchange(S publisher, Class<T> elementClass);
}
/**
* Specification for decoding the response of an exchange.
*/
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
*/
<T> ExchangeResult<T> decodeEntity(Class<T> entityClass);
/**
* 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[])
*/
<T> ExchangeResult<T> decodeEntity(ResolvableType entityType);
/**
* 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
*/
<T> ExchangeResult<List<T>> decodeAndCollect(Class<T> elementClass);
/**
* 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[])
*/
<T> ExchangeResult<List<T>> decodeAndCollect(ResolvableType 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
*/
<T> ExchangeResult<Flux<T>> decodeFlux(Class<T> elementClass);
/**
* 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[])
*/
<T> ExchangeResult<Flux<T>> decodeFlux(ResolvableType elementType);
/**
* Consume the response and verify there is no content.
* @return container for the result of the exchange
*/
ExchangeResult<Void> expectNoBody();
}
}