ExchangeResult refactoring in WebTestClient
The WebTestClient API no longer provides access to a base ExchangeResult without a decoded response body. Instead the response has to be decoded first and tests can then access the EntityExchangeResult and FluxExchangeResult sub-types.
This commit is contained in:
parent
63118c1ea7
commit
813d3efe61
|
@ -24,7 +24,6 @@ import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
|
@ -291,7 +290,7 @@ class DefaultWebTestClient implements WebTestClient {
|
||||||
|
|
||||||
public <T> FluxExchangeResult<T> decodeBody(ResolvableType elementType) {
|
public <T> FluxExchangeResult<T> decodeBody(ResolvableType elementType) {
|
||||||
Flux<T> body = this.response.body(toFlux(elementType));
|
Flux<T> body = this.response.body(toFlux(elementType));
|
||||||
return new FluxExchangeResult<>(this, body);
|
return new FluxExchangeResult<>(this, body, getTimeout());
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
@ -340,17 +339,6 @@ class DefaultWebTestClient implements WebTestClient {
|
||||||
public BodySpec expectBody() {
|
public BodySpec expectBody() {
|
||||||
return new DefaultBodySpec(this.result);
|
return new DefaultBodySpec(this.result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public ResponseSpec consumeWith(Consumer<ExchangeResult> consumer) {
|
|
||||||
this.result.assertWithDiagnostics(() -> consumer.accept(this.result));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ExchangeResult returnResult() {
|
|
||||||
return this.result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DefaultTypeBodySpec implements TypeBodySpec {
|
private class DefaultTypeBodySpec implements TypeBodySpec {
|
||||||
|
|
|
@ -37,7 +37,7 @@ public class EntityExchangeResult<T> extends ExchangeResult {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the body extracted from the response.
|
* Return the entity extracted from the response body.
|
||||||
*/
|
*/
|
||||||
public T getResponseBody() {
|
public T getResponseBody() {
|
||||||
return this.body;
|
return this.body;
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.springframework.test.web.reactive.server;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
@ -29,20 +30,18 @@ import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseCookie;
|
import org.springframework.http.ResponseCookie;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides access to request and response details from an exchange performed
|
* Container for request and response details for exchanges performed through
|
||||||
* through the {@link WebTestClient}.
|
* {@link WebTestClient}.
|
||||||
*
|
*
|
||||||
* <p>When an {@code ExchangeResult} is first created it has the status and the
|
* <p>Note that a decoded response body is not exposed at this level since the
|
||||||
* headers of the response ready. Later when the response body is extracted,
|
* body may not have been decoded and consumed yet. Sub-types
|
||||||
* the {@code ExchangeResult} is re-created as {@link EntityExchangeResult} or
|
* {@link EntityExchangeResult} and {@link FluxExchangeResult} provide access
|
||||||
* {@link FluxExchangeResult} also exposing the extracted entities.
|
* to a decoded response entity and a decoded (but not consumed) response body
|
||||||
*
|
* respectively.
|
||||||
* <p>Serialized request and response content may also be accessed through the
|
|
||||||
* methods {@link #getRequestContent()} and {@link #getResponseContent()} after
|
|
||||||
* that content has been fully read or written.
|
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @since 5.0
|
* @since 5.0
|
||||||
|
@ -53,8 +52,8 @@ import org.springframework.util.MultiValueMap;
|
||||||
public class ExchangeResult {
|
public class ExchangeResult {
|
||||||
|
|
||||||
private static final List<MediaType> PRINTABLE_MEDIA_TYPES = Arrays.asList(
|
private static final List<MediaType> PRINTABLE_MEDIA_TYPES = Arrays.asList(
|
||||||
MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.parseMediaType("text/*"),
|
MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML,
|
||||||
MediaType.APPLICATION_FORM_URLENCODED);
|
MediaType.parseMediaType("text/*"), MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
|
||||||
|
|
||||||
private final WiretapClientHttpRequest request;
|
private final WiretapClientHttpRequest request;
|
||||||
|
@ -101,12 +100,16 @@ public class ExchangeResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a "promise" for the raw request body content once completed.
|
* Return the raw request body content written as a {@code byte[]}.
|
||||||
|
* @throws IllegalStateException if the request body is not fully written yet.
|
||||||
*/
|
*/
|
||||||
public MonoProcessor<byte[]> getRequestContent() {
|
public byte[] getRequestBodyContent() {
|
||||||
return this.request.getBodyContent();
|
MonoProcessor<byte[]> body = this.request.getRecordedContent();
|
||||||
|
Assert.isTrue(body.isTerminated(), "Request body incomplete.");
|
||||||
|
return body.block(Duration.ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the status of the executed request.
|
* Return the status of the executed request.
|
||||||
*/
|
*/
|
||||||
|
@ -129,10 +132,13 @@ public class ExchangeResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a "promise" for the raw response body content once completed.
|
* Return the raw request body content written as a {@code byte[]}.
|
||||||
|
* @throws IllegalStateException if the response is not fully read yet.
|
||||||
*/
|
*/
|
||||||
public MonoProcessor<byte[]> getResponseContent() {
|
public byte[] getResponseBodyContent() {
|
||||||
return this.response.getBodyContent();
|
MonoProcessor<byte[]> body = this.response.getRecordedContent();
|
||||||
|
Assert.state(body.isTerminated(), "Response body incomplete.");
|
||||||
|
return body.block(Duration.ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -157,12 +163,12 @@ public class ExchangeResult {
|
||||||
"> " + getMethod() + " " + getUrl() + "\n" +
|
"> " + getMethod() + " " + getUrl() + "\n" +
|
||||||
"> " + formatHeaders(getRequestHeaders(), "\n> ") + "\n" +
|
"> " + formatHeaders(getRequestHeaders(), "\n> ") + "\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
formatBody(getRequestHeaders().getContentType(), getRequestContent()) + "\n" +
|
formatBody(getRequestHeaders().getContentType(), this.request.getRecordedContent()) + "\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"< " + getStatus() + " " + getStatusReason() + "\n" +
|
"< " + getStatus() + " " + getStatusReason() + "\n" +
|
||||||
"< " + formatHeaders(getResponseHeaders(), "\n< ") + "\n" +
|
"< " + formatHeaders(getResponseHeaders(), "\n< ") + "\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
formatBody(getResponseHeaders().getContentType(), getResponseContent()) + "\n\n";
|
formatBody(getResponseHeaders().getContentType(), this.response.getRecordedContent()) + "\n\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getStatusReason() {
|
private String getStatusReason() {
|
||||||
|
|
|
@ -15,10 +15,14 @@
|
||||||
*/
|
*/
|
||||||
package org.springframework.test.web.reactive.server;
|
package org.springframework.test.web.reactive.server;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@code ExchangeResult} variant with the response body as a {@code Flux<T>}.
|
* {@code ExchangeResult} variant with the response body decoded as
|
||||||
|
* {@code Flux<T>} but not yet consumed.
|
||||||
*
|
*
|
||||||
* @param <T> the type of elements in the response body
|
* @param <T> the type of elements in the response body
|
||||||
*
|
*
|
||||||
|
@ -28,20 +32,65 @@ import reactor.core.publisher.Flux;
|
||||||
*/
|
*/
|
||||||
public class FluxExchangeResult<T> extends ExchangeResult {
|
public class FluxExchangeResult<T> extends ExchangeResult {
|
||||||
|
|
||||||
|
private static final IllegalStateException TIMEOUT_ERROR =
|
||||||
|
new IllegalStateException("Response timeout: for infinite streams " +
|
||||||
|
"use getResponseBody() first with explicit cancellation, e.g. via take(n).");
|
||||||
|
|
||||||
|
|
||||||
private final Flux<T> body;
|
private final Flux<T> body;
|
||||||
|
|
||||||
|
private final Duration timeout;
|
||||||
|
|
||||||
FluxExchangeResult(ExchangeResult result, Flux<T> body) {
|
|
||||||
|
FluxExchangeResult(ExchangeResult result, Flux<T> body, Duration timeout) {
|
||||||
super(result);
|
super(result);
|
||||||
this.body = body;
|
this.body = body;
|
||||||
|
this.timeout = timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the {@code Flux} of elements decoded from the response body.
|
* Return the response body as a {@code Flux<T>} of decoded elements.
|
||||||
|
*
|
||||||
|
* <p>The response body stream can then be consumed further with the
|
||||||
|
* "reactor-test" {@code StepVerifier} and cancelled when enough elements have been
|
||||||
|
* consumed from the (possibly infinite) stream:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* FluxExchangeResult<Person> result = this.client.get()
|
||||||
|
* .uri("/persons")
|
||||||
|
* .accept(TEXT_EVENT_STREAM)
|
||||||
|
* .exchange()
|
||||||
|
* .expectStatus().isOk()
|
||||||
|
* .expectHeader().contentType(TEXT_EVENT_STREAM)
|
||||||
|
* .expectBody(Person.class)
|
||||||
|
* .returnResult();
|
||||||
|
*
|
||||||
|
* StepVerifier.create(result.getResponseBody())
|
||||||
|
* .expectNext(new Person("Jane"), new Person("Jason"))
|
||||||
|
* .expectNextCount(4)
|
||||||
|
* .expectNext(new Person("Jay"))
|
||||||
|
* .thenCancel()
|
||||||
|
* .verify();
|
||||||
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public Flux<T> getResponseBody() {
|
public Flux<T> getResponseBody() {
|
||||||
return this.body;
|
return this.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
* <p><strong>Note:</strong> this method should typically be called after
|
||||||
|
* the response has been consumed in full via {@link #getResponseBody()}.
|
||||||
|
* Calling it first will cause the response {@code Flux<T>} to be consumed
|
||||||
|
* via {@code getResponseBody.ignoreElements()}.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public byte[] getResponseBodyContent() {
|
||||||
|
return this.body.ignoreElements()
|
||||||
|
.timeout(this.timeout, Mono.error(TIMEOUT_ERROR))
|
||||||
|
.then(() -> Mono.just(super.getResponseBodyContent()))
|
||||||
|
.block();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -483,19 +483,6 @@ public interface WebTestClient {
|
||||||
*/
|
*/
|
||||||
BodySpec expectBody();
|
BodySpec expectBody();
|
||||||
|
|
||||||
/**
|
|
||||||
* Consume request and response details of the exchange. Only status
|
|
||||||
* response headers are available at this stage before one of the
|
|
||||||
* {@code expectBody} methods is used.
|
|
||||||
*/
|
|
||||||
ResponseSpec consumeWith(Consumer<ExchangeResult> consumer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the request and response details of the exchange. Only status
|
|
||||||
* and response headers are available at this stage before one of the
|
|
||||||
* {@code expectBody} methods is used.
|
|
||||||
*/
|
|
||||||
ExchangeResult returnResult();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -52,7 +52,7 @@ class WiretapClientHttpRequest extends ClientHttpRequestDecorator {
|
||||||
/**
|
/**
|
||||||
* Return a "promise" with the request body content written to the server.
|
* Return a "promise" with the request body content written to the server.
|
||||||
*/
|
*/
|
||||||
public MonoProcessor<byte[]> getBodyContent() {
|
public MonoProcessor<byte[]> getRecordedContent() {
|
||||||
return this.body;
|
return this.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ class WiretapClientHttpResponse extends ClientHttpResponseDecorator {
|
||||||
/**
|
/**
|
||||||
* Return a "promise" with the response body content read from the server.
|
* Return a "promise" with the response body content read from the server.
|
||||||
*/
|
*/
|
||||||
public MonoProcessor<byte[]> getBodyContent() {
|
public MonoProcessor<byte[]> getRecordedContent() {
|
||||||
return this.body;
|
return this.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue