WebTestClient polish and minor refactoring

This commit is contained in:
Rossen Stoyanchev 2017-02-19 20:00:19 -05:00
parent d924538211
commit 20be40bf64
9 changed files with 420 additions and 468 deletions

View File

@ -22,12 +22,9 @@ import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
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;
@ -36,7 +33,6 @@ 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;
@ -48,9 +44,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 java.util.stream.Collectors.toList;
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;
@ -65,33 +61,30 @@ class DefaultWebTestClient implements WebTestClient {
private final WebClient webClient;
private final Duration responseTimeout;
private final WebTestClientConnector webTestClientConnector;
private final WiretapConnectorListener connectorListener;
private final Duration timeout;
private final AtomicLong requestIndex = new AtomicLong();
DefaultWebTestClient(WebClient.Builder webClientBuilder, ClientHttpConnector connector, Duration timeout) {
Assert.notNull(webClientBuilder, "WebClient.Builder is required");
WiretapConnector wiretapConnector = new WiretapConnector(connector);
webClientBuilder.clientConnector(wiretapConnector);
this.connectorListener = new WiretapConnectorListener();
wiretapConnector.addListener(this.connectorListener);
this.webClient = webClientBuilder.build();
this.responseTimeout = (timeout != null ? timeout : Duration.ofSeconds(5));
this.webTestClientConnector = new WebTestClientConnector(connector);
this.webClient = webClientBuilder.clientConnector(this.webTestClientConnector).build();
this.timeout = (timeout != null ? timeout : Duration.ofSeconds(5));
}
private DefaultWebTestClient(DefaultWebTestClient webTestClient, ExchangeFilterFunction filter) {
this.webClient = webTestClient.webClient.filter(filter);
this.connectorListener = webTestClient.connectorListener;
this.responseTimeout = webTestClient.responseTimeout;
this.timeout = webTestClient.timeout;
this.webTestClientConnector = webTestClient.webTestClientConnector;
}
private Duration getTimeout() {
return this.responseTimeout;
return this.timeout;
}
@ -141,7 +134,6 @@ class DefaultWebTestClient implements WebTestClient {
}
private class DefaultUriSpec implements UriSpec {
private final WebClient.UriSpec uriSpec;
@ -181,7 +173,8 @@ class DefaultWebTestClient implements WebTestClient {
DefaultHeaderSpec(WebClient.HeaderSpec spec) {
this.headerSpec = spec;
this.requestId = connectorListener.registerRequestId(spec);
this.requestId = String.valueOf(requestIndex.incrementAndGet());
this.headerSpec.header(WebTestClientConnector.REQUEST_ID_HEADER_NAME, this.requestId);
}
@ -247,29 +240,25 @@ class DefaultWebTestClient implements WebTestClient {
@Override
public ResponseSpec exchange() {
return createResponseSpec(this.headerSpec.exchange());
return toResponseSpec(this.headerSpec.exchange());
}
@Override
public <T> ResponseSpec exchange(BodyInserter<T, ? super ClientHttpRequest> inserter) {
return createResponseSpec(this.headerSpec.exchange(inserter));
return toResponseSpec(this.headerSpec.exchange(inserter));
}
@Override
public <T, S extends Publisher<T>> ResponseSpec exchange(S publisher, Class<T> elementClass) {
return createResponseSpec(this.headerSpec.exchange(publisher, elementClass));
return toResponseSpec(this.headerSpec.exchange(publisher, elementClass));
}
protected DefaultResponseSpec createResponseSpec(Mono<ClientResponse> responseMono) {
private DefaultResponseSpec toResponseSpec(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);
ClientHttpRequest request = webTestClientConnector.claimRequest(this.requestId);
ExchangeResult<Flux<DataBuffer>> result = ExchangeResult.create(request, response);
return new DefaultResponseSpec(result, response);
}
}
private abstract class ResponseSpecSupport {
@ -284,11 +273,6 @@ class DefaultWebTestClient implements WebTestClient {
this.response = response;
}
public ResponseSpecSupport(ResponseSpecSupport responseSpec) {
this.exchangeResult = responseSpec.getExchangeResult();
this.response = responseSpec.getResponse();
}
protected ExchangeResult<Flux<DataBuffer>> getExchangeResult() {
return this.exchangeResult;
@ -298,10 +282,6 @@ class DefaultWebTestClient implements WebTestClient {
return this.response;
}
protected HttpHeaders getResponseHeaders() {
return getExchangeResult().getResponseHeaders();
}
protected <T> ExchangeResult<T> createResultWithDecodedBody(T body) {
return ExchangeResult.withDecodedBody(this.exchangeResult, body);
}
@ -311,19 +291,28 @@ class DefaultWebTestClient implements WebTestClient {
private class DefaultResponseSpec extends ResponseSpecSupport implements ResponseSpec {
public DefaultResponseSpec(ExchangeResult<Flux<DataBuffer>> result, ClientResponse response) {
super(result, response);
public DefaultResponseSpec(ExchangeResult<Flux<DataBuffer>> exchangeResult, ClientResponse response) {
super(exchangeResult, response);
}
@Override
public StatusAssertions expectStatus() {
return new StatusAssertions(getResponse().statusCode(), this);
return new StatusAssertions(getExchangeResult(), this);
}
@Override
public HeaderAssertions expectHeader() {
return new HeaderAssertions(getResponseHeaders(), this);
return new HeaderAssertions(getExchangeResult(), this);
}
@Override
public TypeBodySpec expectBody(Class<?> elementType) {
return expectBody(ResolvableType.forClass(elementType));
}
@Override
public TypeBodySpec expectBody(ResolvableType elementType) {
return new DefaultTypeBodySpec(this, elementType);
}
@Override
@ -331,16 +320,6 @@ class DefaultWebTestClient implements WebTestClient {
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());
@ -353,97 +332,13 @@ class DefaultWebTestClient implements WebTestClient {
}
}
private class DefaultBodySpec extends ResponseSpecSupport implements BodySpec {
public DefaultBodySpec(ResponseSpecSupport responseSpec) {
super(responseSpec);
}
@Override
public ExchangeResult<Void> isEmpty() {
DataBuffer buffer = getResponse().body(toDataBuffers()).blockFirst(getTimeout());
assertTrue("Expected empty body", buffer == null);
return createResultWithDecodedBody(null);
}
@Override
public MapBodySpec map(Class<?> keyType, Class<?> valueType) {
return map(ResolvableType.forClass(keyType), ResolvableType.forClass(valueType));
}
@Override
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 MapBodySpec hasSize(int size) {
assertEquals("Response body map size", size, this.body.size());
return this;
}
@Override
public MapBodySpec contains(Object key, Object value) {
assertEquals("Response body map value for key " + key, value, this.body.get(key));
return this;
}
@Override
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 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;
}
@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 class DefaultTypeBodySpec extends ResponseSpecSupport implements TypeBodySpec {
private final ResolvableType elementType;
public DefaultElementBodySpec(ResponseSpecSupport spec, ResolvableType elementType) {
super(spec);
public DefaultTypeBodySpec(DefaultResponseSpec spec, ResolvableType elementType) {
super(spec.getExchangeResult(), spec.getResponse());
this.elementType = elementType;
}
@ -455,7 +350,7 @@ class DefaultWebTestClient implements WebTestClient {
@Override
public ListBodySpec list() {
return new DefaultListBodySpec(this, this.elementType, -1);
return list(-1);
}
@Override
@ -470,14 +365,13 @@ class DefaultWebTestClient implements WebTestClient {
}
}
private class DefaultSingleValueBodySpec extends ResponseSpecSupport
implements SingleValueBodySpec {
private class DefaultSingleValueBodySpec extends ResponseSpecSupport implements SingleValueBodySpec {
private final Object body;
public DefaultSingleValueBodySpec(ResponseSpecSupport spec, ResolvableType elementType) {
super(spec);
public DefaultSingleValueBodySpec(DefaultTypeBodySpec spec, ResolvableType elementType) {
super(spec.getExchangeResult(), spec.getResponse());
this.body = getResponse().body(toMono(elementType)).block(getTimeout());
}
@ -495,14 +389,13 @@ class DefaultWebTestClient implements WebTestClient {
}
}
private class DefaultListBodySpec extends ResponseSpecSupport
implements ListBodySpec {
private class DefaultListBodySpec extends ResponseSpecSupport implements ListBodySpec {
private final List<?> body;
public DefaultListBodySpec(ResponseSpecSupport spec, ResolvableType elementType, int elementCount) {
super(spec);
public DefaultListBodySpec(DefaultTypeBodySpec spec, ResolvableType elementType, int elementCount) {
super(spec.getExchangeResult(), spec.getResponse());
Flux<?> flux = getResponse().body(toFlux(elementType));
if (elementCount >= 0) {
flux = flux.take(elementCount);
@ -545,33 +438,79 @@ class DefaultWebTestClient implements WebTestClient {
}
}
private static class WiretapConnectorListener implements Consumer<WiretapConnector.Info> {
private static final String REQUEST_ID_HEADER_NAME = "request-id";
private class DefaultBodySpec extends ResponseSpecSupport implements BodySpec {
private final AtomicLong index = new AtomicLong();
private final Map<String, WiretapConnector.Info> exchanges = new ConcurrentHashMap<>();
public DefaultBodySpec(DefaultResponseSpec spec) {
super(spec.getExchangeResult(), spec.getResponse());
}
public String registerRequestId(WebClient.HeaderSpec headerSpec) {
String requestId = String.valueOf(this.index.incrementAndGet());
headerSpec.header(REQUEST_ID_HEADER_NAME, requestId);
return requestId;
@Override
public ExchangeResult<Void> isEmpty() {
DataBuffer buffer = getResponse().body(toDataBuffers()).blockFirst(getTimeout());
assertTrue("Expected empty body", buffer == null);
return createResultWithDecodedBody(null);
}
@Override
public void accept(WiretapConnector.Info info) {
Optional.ofNullable(info.getRequestHeaders().getFirst(REQUEST_ID_HEADER_NAME))
.ifPresent(id -> this.exchanges.put(id, info));
public MapBodySpec map(Class<?> keyType, Class<?> valueType) {
return map(ResolvableType.forClass(keyType), ResolvableType.forClass(valueType));
}
public WiretapConnector.Info retrieveRequest(String requestId) {
WiretapConnector.Info info = this.exchanges.remove(requestId);
Assert.notNull(info, "No match for request-id=" + requestId);
return info;
@Override
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(DefaultBodySpec spec, ResolvableType keyType, ResolvableType valueType) {
super(spec.getExchangeResult(), spec.getResponse());
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 MapBodySpec hasSize(int size) {
assertEquals("Response body map size", size, this.body.size());
return this;
}
@Override
public MapBodySpec contains(Object key, Object value) {
assertEquals("Response body map value for key " + key, value, this.body.get(key));
return this;
}
@Override
public MapBodySpec containsKeys(Object... keys) {
List<?> missing = Arrays.stream(keys).filter(k -> !this.body.containsKey(k)).collect(toList());
assertTrue("Response body map does not contain keys " + missing, missing.isEmpty());
return this;
}
@Override
public MapBodySpec containsValues(Object... values) {
List<?> missing = Arrays.stream(values).filter(v -> !this.body.containsValue(v)).collect(toList());
assertTrue("Response body map does not contain values " + missing, missing.isEmpty());
return this;
}
@Override
@SuppressWarnings("unchecked")
public <K, V> ExchangeResult<Map<K, V>> returnResult() {
return createResultWithDecodedBody((Map<K, V>) this.body);
}
}

View File

@ -24,6 +24,7 @@ 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.http.client.reactive.ClientHttpRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import static org.springframework.web.reactive.function.BodyExtractors.toDataBuffers;
@ -111,19 +112,16 @@ public class ExchangeResult<T> {
/**
* Create an instance from a ClientResponse (body not yet consumed).
* Create from ClientHttpRequest and 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);
static ExchangeResult<Flux<DataBuffer>> create(ClientHttpRequest request, ClientResponse response) {
return new ExchangeResult<>(request.getMethod(), request.getURI(), request.getHeaders(),
response.statusCode(), response.headers().asHttpHeaders(),
response.body(toDataBuffers()));
}
/**
* Re-create the result with a generic type matching the decoded body.
* Re-create with decoded body (possibly still not consumed).
*/
static <T> ExchangeResult<T> withDecodedBody(ExchangeResult<?> result, T body) {
return new ExchangeResult<>(result.getMethod(), result.getUrl(), result.getRequestHeaders(),

View File

@ -29,33 +29,42 @@ import static org.springframework.test.util.AssertionErrors.assertEquals;
import static org.springframework.test.util.AssertionErrors.assertTrue;
/**
* Provides methods for HTTP header assertions.
* Assertions on headers of the response.
*
* @author Rossen Stoyanchev
* @since 5.0
* @see ResponseAssertions#header()
* @see WebTestClient.ResponseSpec#expectHeader()
*/
public class HeaderAssertions {
private final HttpHeaders headers;
private final ExchangeResult<?> exchangeResult;
private final WebTestClient.ResponseSpec responseSpec;
public HeaderAssertions(HttpHeaders headers, WebTestClient.ResponseSpec responseSpec) {
this.headers = headers;
HeaderAssertions(ExchangeResult<?> exchangeResult, WebTestClient.ResponseSpec responseSpec) {
this.exchangeResult = exchangeResult;
this.responseSpec = responseSpec;
}
/**
* Expect a header with the given name to match the specified values.
*/
public WebTestClient.ResponseSpec valueEquals(String headerName, String... values) {
List<String> actual = this.headers.get(headerName);
List<String> actual = getHeaders().get(headerName);
assertEquals("Response header [" + headerName + "]", Arrays.asList(values), actual);
return this.responseSpec;
}
/**
* Expect a header with the given name whose first value matches the
* provided regex pattern.
* @param headerName the header name
* @param pattern String pattern to pass to {@link Pattern#compile(String)}
*/
public WebTestClient.ResponseSpec valueMatches(String headerName, String pattern) {
List<String> values = this.headers.get(headerName);
List<String> values = getHeaders().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;
@ -63,40 +72,65 @@ public class HeaderAssertions {
return this.responseSpec;
}
public WebTestClient.ResponseSpec cacheControlEquals(CacheControl cacheControl) {
String actual = this.headers.getCacheControl();
/**
* Expect a "Cache-Control" header with the given value.
*/
public WebTestClient.ResponseSpec cacheControl(CacheControl cacheControl) {
String actual = getHeaders().getCacheControl();
assertEquals("Response header Cache-Control", cacheControl.getHeaderValue(), actual);
return this.responseSpec;
}
public WebTestClient.ResponseSpec contentDispositionEquals(ContentDisposition contentDisposition) {
ContentDisposition actual = this.headers.getContentDisposition();
/**
* Expect a "Content-Disposition" header with the given value.
*/
public WebTestClient.ResponseSpec contentDisposition(ContentDisposition contentDisposition) {
ContentDisposition actual = getHeaders().getContentDisposition();
assertEquals("Response header Content-Disposition", contentDisposition, actual);
return this.responseSpec;
}
public WebTestClient.ResponseSpec contentLengthEquals(long contentLength) {
long actual = this.headers.getContentLength();
/**
* Expect a "Content-Length" header with the given value.
*/
public WebTestClient.ResponseSpec contentLength(long contentLength) {
long actual = getHeaders().getContentLength();
assertEquals("Response header Content-Length", contentLength, actual);
return this.responseSpec;
}
public WebTestClient.ResponseSpec contentTypeEquals(MediaType mediaType) {
MediaType actual = this.headers.getContentType();
/**
* Expect a "Content-Type" header with the given value.
*/
public WebTestClient.ResponseSpec contentType(MediaType mediaType) {
MediaType actual = getHeaders().getContentType();
assertEquals("Response header Content-Type", mediaType, actual);
return this.responseSpec;
}
public WebTestClient.ResponseSpec expiresEquals(int expires) {
long actual = this.headers.getExpires();
/**
* Expect an "Expires" header with the given value.
*/
public WebTestClient.ResponseSpec expires(int expires) {
long actual = getHeaders().getExpires();
assertEquals("Response header Expires", expires, actual);
return this.responseSpec;
}
public WebTestClient.ResponseSpec lastModifiedEquals(int lastModified) {
long actual = this.headers.getLastModified();
/**
* Expect a "Last-Modified" header with the given value.
*/
public WebTestClient.ResponseSpec lastModified(int lastModified) {
long actual = getHeaders().getLastModified();
assertEquals("Response header Last-Modified", lastModified, actual);
return this.responseSpec;
}
// Private methods
private HttpHeaders getHeaders() {
return this.exchangeResult.getResponseHeaders();
}
}

View File

@ -20,22 +20,22 @@ import org.springframework.http.HttpStatus;
import static org.springframework.test.util.AssertionErrors.assertEquals;
/**
* Assertions on the status of a response.
* Assertions on the response status.
*
* @author Rossen Stoyanchev
* @since 5.0
* @see ResponseAssertions#status()
* @see WebTestClient.ResponseSpec#expectStatus()
*/
@SuppressWarnings("unused")
public class StatusAssertions {
private final HttpStatus httpStatus;
private final ExchangeResult<?> exchangeResult;
private final WebTestClient.ResponseSpec responseSpec;
StatusAssertions(HttpStatus status, WebTestClient.ResponseSpec responseSpec) {
this.httpStatus = status;
StatusAssertions(ExchangeResult<?> exchangeResult, WebTestClient.ResponseSpec responseSpec) {
this.exchangeResult = exchangeResult;
this.responseSpec = responseSpec;
}
@ -44,7 +44,7 @@ public class StatusAssertions {
* Assert the response status as an {@link HttpStatus}.
*/
public WebTestClient.ResponseSpec isEqualTo(HttpStatus status) {
assertEquals("Response status", status, this.httpStatus);
assertEquals("Response status", status, getStatus());
return this.responseSpec;
}
@ -52,7 +52,7 @@ public class StatusAssertions {
* Assert the response status as an integer.
*/
public WebTestClient.ResponseSpec isEqualTo(int status) {
assertEquals("Response status", status, this.httpStatus.value());
assertEquals("Response status", status, getStatus().value());
return this.responseSpec;
}
@ -60,7 +60,7 @@ public class StatusAssertions {
* Assert the response status code is {@code HttpStatus.OK} (200).
*/
public WebTestClient.ResponseSpec isOk() {
assertEquals("Status", HttpStatus.OK, this.httpStatus);
assertEquals("Status", HttpStatus.OK, getStatus());
return this.responseSpec;
}
@ -68,7 +68,7 @@ public class StatusAssertions {
* Assert the response status code is {@code HttpStatus.CREATED} (201).
*/
public WebTestClient.ResponseSpec isCreated() {
assertEquals("Status", HttpStatus.CREATED, this.httpStatus);
assertEquals("Status", HttpStatus.CREATED, getStatus());
return this.responseSpec;
}
@ -76,7 +76,7 @@ public class StatusAssertions {
* Assert the response status code is {@code HttpStatus.ACCEPTED} (202).
*/
public WebTestClient.ResponseSpec isAccepted() {
assertEquals("Status", HttpStatus.ACCEPTED, this.httpStatus);
assertEquals("Status", HttpStatus.ACCEPTED, getStatus());
return this.responseSpec;
}
@ -84,7 +84,7 @@ public class StatusAssertions {
* Assert the response status code is {@code HttpStatus.NO_CONTENT} (204).
*/
public WebTestClient.ResponseSpec isNoContent() {
assertEquals("Status", HttpStatus.NO_CONTENT, this.httpStatus);
assertEquals("Status", HttpStatus.NO_CONTENT, getStatus());
return this.responseSpec;
}
@ -92,7 +92,7 @@ public class StatusAssertions {
* Assert the response status code is {@code HttpStatus.FOUND} (302).
*/
public WebTestClient.ResponseSpec isFound() {
assertEquals("Status", HttpStatus.FOUND, this.httpStatus);
assertEquals("Status", HttpStatus.FOUND, getStatus());
return this.responseSpec;
}
@ -100,7 +100,7 @@ public class StatusAssertions {
* Assert the response status code is {@code HttpStatus.SEE_OTHER} (303).
*/
public WebTestClient.ResponseSpec isSeeOther() {
assertEquals("Status", HttpStatus.SEE_OTHER, this.httpStatus);
assertEquals("Status", HttpStatus.SEE_OTHER, getStatus());
return this.responseSpec;
}
@ -108,7 +108,7 @@ public class StatusAssertions {
* Assert the response status code is {@code HttpStatus.NOT_MODIFIED} (304).
*/
public WebTestClient.ResponseSpec isNotModified() {
assertEquals("Status", HttpStatus.NOT_MODIFIED, this.httpStatus);
assertEquals("Status", HttpStatus.NOT_MODIFIED, getStatus());
return this.responseSpec;
}
@ -116,7 +116,7 @@ public class StatusAssertions {
* Assert the response status code is {@code HttpStatus.TEMPORARY_REDIRECT} (307).
*/
public WebTestClient.ResponseSpec isTemporaryRedirect() {
assertEquals("Status", HttpStatus.TEMPORARY_REDIRECT, this.httpStatus);
assertEquals("Status", HttpStatus.TEMPORARY_REDIRECT, getStatus());
return this.responseSpec;
}
@ -124,7 +124,7 @@ public class StatusAssertions {
* Assert the response status code is {@code HttpStatus.PERMANENT_REDIRECT} (308).
*/
public WebTestClient.ResponseSpec isPermanentRedirect() {
assertEquals("Status", HttpStatus.PERMANENT_REDIRECT, this.httpStatus);
assertEquals("Status", HttpStatus.PERMANENT_REDIRECT, getStatus());
return this.responseSpec;
}
@ -132,7 +132,15 @@ public class StatusAssertions {
* Assert the response status code is {@code HttpStatus.BAD_REQUEST} (400).
*/
public WebTestClient.ResponseSpec isBadRequest() {
assertEquals("Status", HttpStatus.BAD_REQUEST, this.httpStatus);
assertEquals("Status", HttpStatus.BAD_REQUEST, getStatus());
return this.responseSpec;
}
/**
* Assert the response status code is {@code HttpStatus.UNAUTHORIZED} (401).
*/
public WebTestClient.ResponseSpec isUnauthorized() {
assertEquals("Status", HttpStatus.UNAUTHORIZED, getStatus());
return this.responseSpec;
}
@ -140,7 +148,7 @@ public class StatusAssertions {
* Assert the response status code is {@code HttpStatus.NOT_FOUND} (404).
*/
public WebTestClient.ResponseSpec isNotFound() {
assertEquals("Status", HttpStatus.NOT_FOUND, this.httpStatus);
assertEquals("Status", HttpStatus.NOT_FOUND, getStatus());
return this.responseSpec;
}
@ -148,7 +156,7 @@ public class StatusAssertions {
* Assert the response error message.
*/
public WebTestClient.ResponseSpec reasonEquals(String reason) {
assertEquals("Response status reason", reason, this.httpStatus.getReasonPhrase());
assertEquals("Response status reason", reason, getStatus().getReasonPhrase());
return this.responseSpec;
}
@ -156,8 +164,8 @@ public class StatusAssertions {
* Assert the response status code is in the 1xx range.
*/
public WebTestClient.ResponseSpec is1xxInformational() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.INFORMATIONAL, this.httpStatus.series());
String message = "Range for response status value " + getStatus();
assertEquals(message, HttpStatus.Series.INFORMATIONAL, getStatus().series());
return this.responseSpec;
}
@ -165,8 +173,8 @@ public class StatusAssertions {
* Assert the response status code is in the 2xx range.
*/
public WebTestClient.ResponseSpec is2xxSuccessful() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.SUCCESSFUL, this.httpStatus.series());
String message = "Range for response status value " + getStatus();
assertEquals(message, HttpStatus.Series.SUCCESSFUL, getStatus().series());
return this.responseSpec;
}
@ -174,8 +182,8 @@ public class StatusAssertions {
* Assert the response status code is in the 3xx range.
*/
public WebTestClient.ResponseSpec is3xxRedirection() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.REDIRECTION, this.httpStatus.series());
String message = "Range for response status value " + getStatus();
assertEquals(message, HttpStatus.Series.REDIRECTION, getStatus().series());
return this.responseSpec;
}
@ -183,8 +191,8 @@ public class StatusAssertions {
* Assert the response status code is in the 4xx range.
*/
public WebTestClient.ResponseSpec is4xxClientError() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.CLIENT_ERROR, this.httpStatus.series());
String message = "Range for response status value " + getStatus();
assertEquals(message, HttpStatus.Series.CLIENT_ERROR, getStatus().series());
return this.responseSpec;
}
@ -192,9 +200,16 @@ public class StatusAssertions {
* Assert the response status code is in the 5xx range.
*/
public WebTestClient.ResponseSpec is5xxServerError() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.SERVER_ERROR, this.httpStatus.series());
String message = "Range for response status value " + getStatus();
assertEquals(message, HttpStatus.Series.SERVER_ERROR, getStatus().series());
return this.responseSpec;
}
// Private methods
private HttpStatus getStatus() {
return this.exchangeResult.getStatus();
}
}

View File

@ -448,7 +448,7 @@ public interface WebTestClient {
}
/**
* Specification for expectations on the response.
* Specification for processing the response and applying expectations.
*/
interface ResponseSpec {
@ -458,40 +458,123 @@ public interface WebTestClient {
StatusAssertions expectStatus();
/**
* Assertions on the response headers.
* Assertions on the headers of the response.
*/
HeaderAssertions expectHeader();
/**
* Assertions on the response body.
* Assertions on the body of the response extracted to one or more
* elements of the given type.
*/
TypeBodySpec expectBody(Class<?> elementType);
/**
* Variant of {@link #expectBody(Class)} for use with generic types.
*/
TypeBodySpec expectBody(ResolvableType elementType);
/**
* Access to additional assertions on the response body --
* isEmpty, map, and others.
*/
BodySpec expectBody();
/**
* Assertions on the response body where the body is to be decoded to
* one or more elements of the given type.
*/
ElementBodySpec expectBody(Class<?> elementType);
/**
* Alternative to {@link #expectBody(Class)} for generic types.
*/
ElementBodySpec expectBody(ResolvableType elementType);
/**
* Consume the result of the exchange and continue with expectations.
* Consume the {@link ExchangeResult} and continue with expectations.
* The {@code ExchangeResult} is parameterized with data buffers since
* the body is not yet consumed nor decoded at this level.
*/
ResponseSpec consumeWith(Consumer<ExchangeResult<Flux<DataBuffer>>> consumer);
/**
* Return a container for the result of the exchange with the body
* not yet decoded nor consumed.
* Return a container for the result of the exchange. The returned
* {@code ExchangeResult} is parameterized with data buffers since
* the body is not yet consumed nor decoded at this level.
*/
ExchangeResult<Flux<DataBuffer>> returnResult();
}
/**
* Specification for expectations on the body of the response.
* Specification for extracting entities from the response body.
*/
interface TypeBodySpec {
/**
* Extract a single value from the response.
*/
SingleValueBodySpec value();
/**
* Extract a list of values from the response.
*/
ListBodySpec list();
/**
* Extract a list of values consuming the first N elements.
*/
ListBodySpec list(int elementCount);
/**
* Return a container for the result of the exchange parameterized with
* the {@code Flux} of decoded objects (not yet consumed).
*/
<T> ExchangeResult<Flux<T>> returnResult();
}
/**
* Specification to assert a single value extracted from the response body.
*/
interface SingleValueBodySpec {
/**
* Assert the extracted body is equal to the given value.
*/
<T> ExchangeResult<T> isEqualTo(Object expected);
/**
* Return a container for the result of the exchange parameterized with
* the extracted response entity.
*/
<T> ExchangeResult<T> returnResult();
}
/**
* Specification to assert a list of values extracted from the response.
*/
interface ListBodySpec {
/**
* Assert the extracted body is equal to the given list.
*/
<T> ExchangeResult<List<T>> isEqualTo(List<T> expected);
/**
* Assert the extracted list of values is of the given size.
* @param size the expected size
*/
ListBodySpec hasSize(int size);
/**
* Assert the extracted list of values contains the given elements.
* @param elements the elements to check
*/
ListBodySpec contains(Object... elements);
/**
* Assert the extracted list of values doesn't contain the given elements.
* @param elements the elements to check
*/
ListBodySpec doesNotContain(Object... elements);
/**
* Return a container for the result of the exchange parameterized with
* the extracted list of response entities.
*/
<T> ExchangeResult<List<T>> returnResult();
}
/**
* Specification to apply additional assertions on the response body.
*/
interface BodySpec {
@ -502,48 +585,48 @@ public interface WebTestClient {
ExchangeResult<Void> isEmpty();
/**
* Decode the response body as a Map with the given key and value type.
* Extract 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.
* Variant of {@link #map(Class, Class)} for use with generic types.
*/
MapBodySpec map(ResolvableType keyType, ResolvableType valueType);
}
/**
* Specification for expectations on the body of the response decoded as a map.
* Specification to assert response the body extracted as a map.
*/
interface MapBodySpec {
/**
* Assert the decoded body is equal to the given list of elements.
* Assert the extracted map 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.
* Assert the extracted map has the given size.
* @param size the expected size
*/
MapBodySpec hasSize(int size);
/**
* Assert the decoded map contains the given key value pair.
* Assert the extracted 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.
* Assert the extracted map contains the given keys.
* @param keys the keys to check
*/
MapBodySpec containsKeys(Object... keys);
/**
* Assert the decoded map contains the given values.
* Assert the extracted map contains the given values.
* @param values the keys to check
*/
MapBodySpec containsValues(Object... values);
@ -554,85 +637,4 @@ public interface WebTestClient {
<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

@ -0,0 +1,85 @@
/*
* 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.net.URI;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.util.Assert;
/**
* Decorate any other {@link ClientHttpConnector} with the purpose of
* intercepting, capturing, and exposing {@code ClientHttpRequest}s reflecting
* the exact and complete details sent to the server.
*
* @author Rossen Stoyanchev
* @since 5.0
* @see HttpHandlerConnector
*/
class WebTestClientConnector implements ClientHttpConnector {
public static final String REQUEST_ID_HEADER_NAME = "request-id";
private final ClientHttpConnector delegate;
private final Map<String, ClientHttpRequest> capturedRequests = new ConcurrentHashMap<>();
public WebTestClientConnector(ClientHttpConnector delegate) {
this.delegate = delegate;
}
@Override
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
AtomicReference<ClientHttpRequest> requestRef = new AtomicReference<>();
return this.delegate
.connect(method, uri, request -> {
requestRef.set(request);
return requestCallback.apply(request);
})
.doOnNext(response -> {
ClientHttpRequest request = requestRef.get();
String id = request.getHeaders().getFirst(REQUEST_ID_HEADER_NAME);
if (id != null) {
this.capturedRequests.put(id, request);
}
});
}
/**
* Retrieve the request with the given "request-id" header.
*/
public ClientHttpRequest claimRequest(String requestId) {
ClientHttpRequest request = this.capturedRequests.get(requestId);
Assert.notNull(request, "No matching request [" + requestId + "]. Did connect return a response yet?");
return request;
}
}

View File

@ -1,117 +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.net.URI;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
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;
import org.springframework.http.client.reactive.ClientHttpResponse;
/**
* Decorates a {@link ClientHttpConnector} in order to capture executed requests
* and responses and notify one or more registered listeners. This is helpful
* for access to the actual {@link ClientHttpRequest} sent and the
* {@link ClientHttpResponse} returned by the server.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
class WiretapConnector implements ClientHttpConnector {
private final ClientHttpConnector delegate;
private final List<Consumer<Info>> listeners;
public WiretapConnector(ClientHttpConnector delegate) {
this.delegate = delegate;
this.listeners = new CopyOnWriteArrayList<>();
}
/**
* Register a listener to consume exchanged requests and responses.
*/
public void addListener(Consumer<Info> consumer) {
this.listeners.add(consumer);
}
@Override
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
AtomicReference<ClientHttpRequest> requestRef = new AtomicReference<>();
return this.delegate
.connect(method, uri, request -> {
requestRef.set(request);
return requestCallback.apply(request);
})
.doOnNext(response -> {
Info info = new Info(requestRef.get(), response);
this.listeners.forEach(consumer -> consumer.accept(info));
});
}
public static class Info {
private final HttpMethod method;
private final URI url;
private final HttpHeaders requestHeaders;
private final ClientHttpResponse response;
public Info(ClientHttpRequest request, ClientHttpResponse response) {
this.method = request.getMethod();
this.url = request.getURI();
this.requestHeaders = request.getHeaders();
this.response = response;
}
public HttpMethod getMethod() {
return this.method;
}
public URI getUrl() {
return this.url;
}
public HttpHeaders getRequestHeaders() {
return this.requestHeaders;
}
public ClientHttpResponse getResponse() {
return this.response;
}
}
}

View File

@ -17,7 +17,6 @@
package org.springframework.test.web.reactive.server;
import java.net.URI;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import reactor.core.publisher.Mono;
@ -35,35 +34,32 @@ 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;
/**
* Unit tests for {@link WiretapConnector}.
* Unit tests for {@link WebTestClientConnector}.
*
* @author Rossen Stoyanchev
*/
public class WiretapConnectorTests {
public class WebTestClientConnectorTests {
@Test
public void listener() throws Exception {
public void captureAndClaim() throws Exception {
ClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, "/test");
ClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK);
ClientHttpConnector connector = (method, uri, fn) -> fn.apply(request).then(Mono.just(response));
AtomicReference<WiretapConnector.Info> infoRef = new AtomicReference<>();
WiretapConnector wiretapConnector = new WiretapConnector(connector);
wiretapConnector.addListener(infoRef::set);
ClientRequest clientRequest = ClientRequest.method(HttpMethod.GET, URI.create("/test"))
.header(WebTestClientConnector.REQUEST_ID_HEADER_NAME, "1").build();
ExchangeFunction exchangeFn = ExchangeFunctions.create(wiretapConnector);
ClientRequest clientRequest = ClientRequest.method(HttpMethod.GET, URI.create("/test")).build();
exchangeFn.exchange(clientRequest).blockMillis(0);
WebTestClientConnector webTestClientConnector = new WebTestClientConnector(connector);
ExchangeFunction function = ExchangeFunctions.create(webTestClientConnector);
function.exchange(clientRequest).blockMillis(0);
WiretapConnector.Info info = infoRef.get();
assertNotNull(info);
assertEquals(HttpMethod.GET, info.getMethod());
assertEquals("/test", info.getUrl().toString());
assertSame(response, info.getResponse());
ClientHttpRequest actual = webTestClientConnector.claimRequest("1");
assertNotNull(actual);
assertEquals(HttpMethod.GET, actual.getMethod());
assertEquals("/test", actual.getURI().toString());
}
}

View File

@ -66,7 +66,7 @@ public class ResponseEntityTests {
this.client.get().uri("/persons/John")
.exchange()
.expectStatus().isOk()
.expectHeader().contentTypeEquals(MediaType.APPLICATION_JSON_UTF8)
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(Person.class).value().isEqualTo(new Person("John"));
}
@ -79,7 +79,7 @@ public class ResponseEntityTests {
this.client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectHeader().contentTypeEquals(MediaType.APPLICATION_JSON_UTF8)
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(Person.class).list().isEqualTo(expected);
}
@ -106,7 +106,7 @@ public class ResponseEntityTests {
.accept(TEXT_EVENT_STREAM)
.exchange()
.expectStatus().isOk()
.expectHeader().contentTypeEquals(TEXT_EVENT_STREAM)
.expectHeader().contentType(TEXT_EVENT_STREAM)
.expectBody(Person.class)
.returnResult();