diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java index 01085fae75..e6cc98066e 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java @@ -300,10 +300,8 @@ class DefaultWebTestClient implements WebTestClient { } public EntityExchangeResult consumeEmpty() { - assertWithDiagnostics(() -> { - DataBuffer buffer = this.response.body(toDataBuffers()).blockFirst(getTimeout()); - assertTrue("Expected empty body", buffer == null); - }); + DataBuffer buffer = this.response.body(toDataBuffers()).blockFirst(getTimeout()); + assertWithDiagnostics(() -> assertTrue("Expected empty body", buffer == null)); return new EntityExchangeResult<>(this, null); } } @@ -344,10 +342,8 @@ class DefaultWebTestClient implements WebTestClient { @Override public ResponseSpec consumeWith(Consumer consumer) { - return this.result.assertWithDiagnosticsAndReturn(() -> { - consumer.accept(this.result); - return this; - }); + this.result.assertWithDiagnostics(() -> consumer.accept(this.result)); + return this; } @Override @@ -402,10 +398,9 @@ class DefaultWebTestClient implements WebTestClient { @Override public EntityExchangeResult isEqualTo(T expected) { - return this.result.assertWithDiagnosticsAndReturn(() -> { - assertEquals("Response body", expected, this.result.getResponseBody()); - return returnResult(); - }); + Object actual = this.result.getResponseBody(); + this.result.assertWithDiagnostics(() -> assertEquals("Response body", expected, actual)); + return returnResult(); } @SuppressWarnings("unchecked") @@ -427,10 +422,9 @@ class DefaultWebTestClient implements WebTestClient { @Override public EntityExchangeResult> isEqualTo(List expected) { - return this.result.assertWithDiagnosticsAndReturn(() -> { - assertEquals("Response body", expected, this.result.getResponseBody()); - return returnResult(); - }); + List actual = this.result.getResponseBody(); + this.result.assertWithDiagnostics(() -> assertEquals("Response body", expected, actual)); + return returnResult(); } @Override @@ -440,21 +434,19 @@ class DefaultWebTestClient implements WebTestClient { @Override public ListBodySpec contains(Object... elements) { - this.result.assertWithDiagnostics(() -> { - List elementList = Arrays.asList(elements); - String message = "Response body does not contain " + elementList; - assertTrue(message, this.result.getResponseBody().containsAll(elementList)); - }); + List expected = Arrays.asList(elements); + List actual = this.result.getResponseBody(); + String message = "Response body does not contain " + expected; + this.result.assertWithDiagnostics(() -> assertTrue(message, actual.containsAll(expected))); return this; } @Override public ListBodySpec doesNotContain(Object... elements) { - this.result.assertWithDiagnostics(() -> { - List elementList = Arrays.asList(elements); - String message = "Response body should have contained " + elementList; - assertTrue(message, !this.result.getResponseBody().containsAll(Arrays.asList(elements))); - }); + List expected = Arrays.asList(elements); + List actual = this.result.getResponseBody(); + String message = "Response body should have contained " + expected; + this.result.assertWithDiagnostics(() -> assertTrue(message, !actual.containsAll(expected))); return this; } @@ -507,43 +499,38 @@ class DefaultWebTestClient implements WebTestClient { @Override public EntityExchangeResult> isEqualTo(Map expected) { - return this.result.assertWithDiagnosticsAndReturn(() -> { - assertEquals("Response body map", expected, getBody()); - return returnResult(); - }); + String message = "Response body map"; + this.result.assertWithDiagnostics(() -> assertEquals(message, expected, getBody())); + return returnResult(); } @Override public MapBodySpec hasSize(int size) { - return this.result.assertWithDiagnosticsAndReturn(() -> { - assertEquals("Response body map size", size, getBody().size()); - return this; - }); + String message = "Response body map size"; + this.result.assertWithDiagnostics(() -> assertEquals(message, size, getBody().size())); + return this; } @Override public MapBodySpec contains(Object key, Object value) { - return this.result.assertWithDiagnosticsAndReturn(() -> { - assertEquals("Response body map value for key " + key, value, getBody().get(key)); - return this; - }); + String message = "Response body map value for key " + key; + this.result.assertWithDiagnostics(() -> assertEquals(message, value, getBody().get(key))); + return this; } @Override public MapBodySpec containsKeys(Object... keys) { - return this.result.assertWithDiagnosticsAndReturn(() -> { - List missing = Arrays.stream(keys).filter(k -> !getBody().containsKey(k)).collect(toList()); - assertTrue("Response body map does not contain keys " + missing, missing.isEmpty()); - return this; - }); + List missing = Arrays.stream(keys).filter(k -> !getBody().containsKey(k)).collect(toList()); + String message = "Response body map does not contain keys " + missing; + this.result.assertWithDiagnostics(() -> assertTrue(message, missing.isEmpty())); + return this; } @Override public MapBodySpec containsValues(Object... values) { - this.result.assertWithDiagnostics(() -> { - List missing = Arrays.stream(values).filter(v -> !getBody().containsValue(v)).collect(toList()); - assertTrue("Response body map does not contain values " + missing, missing.isEmpty()); - }); + List missing = Arrays.stream(values).filter(v -> !getBody().containsValue(v)).collect(toList()); + String message = "Response body map does not contain values " + missing; + this.result.assertWithDiagnostics(() -> assertTrue(message, missing.isEmpty())); return this; } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java index 734f2e22fd..8f03f4e968 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java @@ -20,7 +20,6 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; -import java.util.function.Supplier; import java.util.stream.Collectors; import reactor.core.publisher.MonoProcessor; @@ -33,16 +32,17 @@ import org.springframework.http.ResponseCookie; import org.springframework.util.MultiValueMap; /** - * Simple container for request and response details from an exchange performed + * Provides access to request and response details from an exchange performed * through the {@link WebTestClient}. * - *

When an {@code ExchangeResult} is first created it has only the status and - * headers of the response available. When the response body is extracted, the - * {@code ExchangeResult} is re-created as either {@link EntityExchangeResult} - * or {@link FluxExchangeResult} that further expose extracted entities. + *

When an {@code ExchangeResult} is first created it has the status and the + * headers of the response ready. Later when the response body is extracted, + * the {@code ExchangeResult} is re-created as {@link EntityExchangeResult} or + * {@link FluxExchangeResult} also exposing the extracted entities. * - *

Raw request and response content may also be accessed once complete via - * {@link #getRequestContent()} or {@link #getResponseContent()}. + *

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 * @since 5.0 @@ -137,8 +137,9 @@ public class ExchangeResult { /** - * Execute the given Runnable in the context of "this" instance and decorate - * any {@link AssertionError}s raised with request and response details. + * Execute the given Runnable, catch any {@link AssertionError}, decorate + * with {@code AssertionError} containing diagnostic information about the + * request and response, and then re-throw. */ public void assertWithDiagnostics(Runnable assertion) { try { @@ -149,32 +150,19 @@ public class ExchangeResult { } } - /** - * Variant of {@link #assertWithDiagnostics(Runnable)} that passes through - * a return value from the assertion code. - */ - public T assertWithDiagnosticsAndReturn(Supplier assertion) { - try { - return assertion.get(); - } - catch (AssertionError ex) { - throw new AssertionError("Assertion failed on the following exchange:" + this, ex); - } - } - @Override public String toString() { return "\n" + "> " + getMethod() + " " + getUrl() + "\n" + - "> " + formatHeaders("> ", getRequestHeaders()) + "\n" + + "> " + formatHeaders(getRequestHeaders(), "\n> ") + "\n" + "\n" + - formatContent(getRequestHeaders().getContentType(), getRequestContent()) + "\n" + + formatBody(getRequestHeaders().getContentType(), getRequestContent()) + "\n" + "\n" + "< " + getStatus() + " " + getStatusReason() + "\n" + - "< " + formatHeaders("< ", getResponseHeaders()) + "\n" + + "< " + formatHeaders(getResponseHeaders(), "\n< ") + "\n" + "\n" + - formatContent(getResponseHeaders().getContentType(), getResponseContent()) + "\n\n"; + formatBody(getResponseHeaders().getContentType(), getResponseContent()) + "\n\n"; } private String getStatusReason() { @@ -185,13 +173,13 @@ public class ExchangeResult { return reason; } - private String formatHeaders(String linePrefix, HttpHeaders headers) { + private String formatHeaders(HttpHeaders headers, String delimiter) { return headers.entrySet().stream() .map(entry -> entry.getKey() + ": " + entry.getValue()) - .collect(Collectors.joining("\n" + linePrefix)); + .collect(Collectors.joining(delimiter)); } - private String formatContent(MediaType contentType, MonoProcessor body) { + private String formatBody(MediaType contentType, MonoProcessor body) { if (body.isSuccess()) { byte[] bytes = body.blockMillis(0); if (bytes.length == 0) { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java index 1a456d84fc..0204abc726 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java @@ -60,13 +60,12 @@ public class HeaderAssertions { * @param pattern String pattern to pass to {@link Pattern#compile(String)} */ public WebTestClient.ResponseSpec valueMatches(String name, String pattern) { - return this.exchangeResult.assertWithDiagnosticsAndReturn(() -> { - String value = getHeaders().getFirst(name); - assertTrue(getMessage(name) + " not found", value != null); - boolean match = Pattern.compile(pattern).matcher(value).matches(); - assertTrue(getMessage(name) + "=\'" + value + "\' does not match \'" + pattern + "\'", match); - return this.responseSpec; - }); + String value = getHeaders().getFirst(name); + assertTrue(getMessage(name) + " not found", value != null); + boolean match = Pattern.compile(pattern).matcher(value).matches(); + String message = getMessage(name) + "=\'" + value + "\' does not match \'" + pattern + "\'"; + this.exchangeResult.assertWithDiagnostics(() -> assertTrue(message, match)); + return this.responseSpec; } /** @@ -123,10 +122,8 @@ public class HeaderAssertions { } private WebTestClient.ResponseSpec assertHeader(String name, Object expected, Object actual) { - return this.exchangeResult.assertWithDiagnosticsAndReturn(() -> { - assertEquals(getMessage(name), expected, actual); - return this.responseSpec; - }); + this.exchangeResult.assertWithDiagnostics(() -> assertEquals(getMessage(name), expected, actual)); + return this.responseSpec; } } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/StatusAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/StatusAssertions.java index c8748fac42..d8d0305248 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/StatusAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/StatusAssertions.java @@ -51,10 +51,9 @@ public class StatusAssertions { * Assert the response status as an integer. */ public WebTestClient.ResponseSpec isEqualTo(int status) { - return this.exchangeResult.assertWithDiagnosticsAndReturn(() -> { - assertEquals("Status", status, this.exchangeResult.getStatus().value()); - return this.responseSpec; - }); + int actual = this.exchangeResult.getStatus().value(); + this.exchangeResult.assertWithDiagnostics(() -> assertEquals("Status", status, actual)); + return this.responseSpec; } /** @@ -146,11 +145,10 @@ public class StatusAssertions { * Assert the response error message. */ public WebTestClient.ResponseSpec reasonEquals(String reason) { - return this.exchangeResult.assertWithDiagnosticsAndReturn(() -> { - HttpStatus status = this.exchangeResult.getStatus(); - assertEquals("Response status reason", reason, status.getReasonPhrase()); - return this.responseSpec; - }); + String actual = this.exchangeResult.getStatus().getReasonPhrase(); + String message = "Response status reason"; + this.exchangeResult.assertWithDiagnostics(() -> assertEquals(message, reason, actual)); + return this.responseSpec; } /** @@ -192,19 +190,16 @@ public class StatusAssertions { // Private methods private WebTestClient.ResponseSpec assertStatusAndReturn(HttpStatus expected) { - return this.exchangeResult.assertWithDiagnosticsAndReturn(() -> { - assertEquals("Status", expected, this.exchangeResult.getStatus()); - return this.responseSpec; - }); + HttpStatus actual = this.exchangeResult.getStatus(); + this.exchangeResult.assertWithDiagnostics(() -> assertEquals("Status", expected, actual)); + return this.responseSpec; } private WebTestClient.ResponseSpec assertSeriesAndReturn(HttpStatus.Series expected) { - return this.exchangeResult.assertWithDiagnosticsAndReturn(() -> { - HttpStatus status = this.exchangeResult.getStatus(); - String message = "Range for response status value " + status; - assertEquals(message, expected, status.series()); - return this.responseSpec; - }); + HttpStatus status = this.exchangeResult.getStatus(); + String message = "Range for response status value " + status; + this.exchangeResult.assertWithDiagnostics(() -> assertEquals(message, expected, status.series())); + return this.responseSpec; } } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index 60cd6599b7..a3538fa2ab 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -512,9 +512,10 @@ public interface WebTestClient { ListBodySpec list(int elementCount); /** - * Return request and response details from the exchange including the - * response body as a {@code Flux} and available for example for use - * with a {@code StepVerifier} from Project Reactor. + * Return request and response details for the exchange incluidng the + * response body decoded as {@code Flux} where {@code } is the + * expected element type. The returned {@code Flux} may for example be + * verified with the Reactor {@code StepVerifier}. */ FluxExchangeResult returnResult(); } @@ -530,7 +531,7 @@ public interface WebTestClient { EntityExchangeResult isEqualTo(T expected); /** - * Return request and response details from the exchange including the + * Return request and response details for the exchange including the * extracted response body. */ EntityExchangeResult returnResult(); @@ -565,7 +566,7 @@ public interface WebTestClient { ListBodySpec doesNotContain(Object... elements); /** - * Return request and response details from the exchange including the + * Return request and response details for the exchange including the * extracted response body. */ EntityExchangeResult> returnResult(); @@ -630,7 +631,7 @@ public interface WebTestClient { MapBodySpec containsValues(Object... values); /** - * Return request and response details from the exchange including the + * Return request and response details for the exchange including the * extracted response body. */ EntityExchangeResult> returnResult(); diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapClientHttpRequest.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapClientHttpRequest.java index 9924848ac6..28f73845d5 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapClientHttpRequest.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapClientHttpRequest.java @@ -27,7 +27,8 @@ import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.http.client.reactive.ClientHttpRequestDecorator; /** - * Client HTTP request decorator that saves the content written to the server. + * Client HTTP request decorator that intercepts and saves content written to + * the server. * * @author Rossen Stoyanchev * @since 5.0 @@ -49,7 +50,7 @@ class WiretapClientHttpRequest extends ClientHttpRequestDecorator { /** - * Return a "promise" for the request body content. + * Return a "promise" with the request body content written to the server. */ public MonoProcessor getBodyContent() { return this.body; @@ -60,39 +61,39 @@ class WiretapClientHttpRequest extends ClientHttpRequestDecorator { public Mono writeWith(Publisher publisher) { return super.writeWith( Flux.from(publisher) - .doOnNext(this::handleBuffer) - .doOnError(this::handleErrorSignal) - .doOnCancel(this::handleCompleteSignal) - .doOnComplete(this::handleCompleteSignal)); + .doOnNext(this::handleOnNext) + .doOnError(this::handleError) + .doOnCancel(this::handleOnComplete) + .doOnComplete(this::handleOnComplete)); } @Override public Mono writeAndFlushWith(Publisher> publisher) { return super.writeAndFlushWith( Flux.from(publisher) - .map(p -> Flux.from(p).doOnNext(this::handleBuffer).doOnError(this::handleErrorSignal)) - .doOnError(this::handleErrorSignal) - .doOnCancel(this::handleCompleteSignal) - .doOnComplete(this::handleCompleteSignal)); + .map(p -> Flux.from(p).doOnNext(this::handleOnNext).doOnError(this::handleError)) + .doOnError(this::handleError) + .doOnCancel(this::handleOnComplete) + .doOnComplete(this::handleOnComplete)); } @Override public Mono setComplete() { - handleCompleteSignal(); + handleOnComplete(); return super.setComplete(); } - private void handleBuffer(DataBuffer buffer) { + private void handleOnNext(DataBuffer buffer) { this.buffer.write(buffer); } - private void handleErrorSignal(Throwable ex) { + private void handleError(Throwable ex) { if (!this.body.isTerminated()) { this.body.onError(ex); } } - private void handleCompleteSignal() { + private void handleOnComplete() { if (!this.body.isTerminated()) { byte[] bytes = new byte[this.buffer.readableByteCount()]; this.buffer.read(bytes); diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapClientHttpResponse.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapClientHttpResponse.java index c4fd81575f..61e2a0dd16 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapClientHttpResponse.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapClientHttpResponse.java @@ -25,7 +25,8 @@ import org.springframework.http.client.reactive.ClientHttpResponse; import org.springframework.http.client.reactive.ClientHttpResponseDecorator; /** - * Client HTTP response decorator that saves the content read from the server. + * Client HTTP response decorator that interceptrs and saves the content read + * from the server. * * @author Rossen Stoyanchev * @since 5.0 @@ -47,7 +48,7 @@ class WiretapClientHttpResponse extends ClientHttpResponseDecorator { /** - * Return a "promise" for the response body content. + * Return a "promise" with the response body content read from the server. */ public MonoProcessor getBodyContent() { return this.body; @@ -58,11 +59,11 @@ class WiretapClientHttpResponse extends ClientHttpResponseDecorator { return super.getBody() .doOnNext(buffer::write) .doOnError(body::onError) - .doOnCancel(this::handleCompleteSignal) - .doOnComplete(this::handleCompleteSignal); + .doOnCancel(this::handleOnComplete) + .doOnComplete(this::handleOnComplete); } - private void handleCompleteSignal() { + private void handleOnComplete() { if (!this.body.isTerminated()) { byte[] bytes = new byte[this.buffer.readableByteCount()]; this.buffer.read(bytes); diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java index 84f2cdf434..35cdf70d22 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java @@ -30,12 +30,13 @@ 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 actual request and response content + * Decorate another {@link ClientHttpConnector} with the purpose of + * intercepting, capturing, and exposing actual request and response data * transmitted to and received from the server. * * @author Rossen Stoyanchev * @since 5.0 + * * @see HttpHandlerConnector */ class WiretapConnector implements ClientHttpConnector { @@ -45,7 +46,7 @@ class WiretapConnector implements ClientHttpConnector { private final ClientHttpConnector delegate; - private final Map capturedExchanges = new ConcurrentHashMap<>(); + private final Map exchanges = new ConcurrentHashMap<>(); public WiretapConnector(ClientHttpConnector delegate) { @@ -66,21 +67,21 @@ class WiretapConnector implements ClientHttpConnector { return requestCallback.apply(wrapped); }) .map(response -> { - WiretapClientHttpRequest request = requestRef.get(); - String requestId = request.getHeaders().getFirst(REQUEST_ID_HEADER_NAME); + WiretapClientHttpRequest wrappedRequest = requestRef.get(); + String requestId = wrappedRequest.getHeaders().getFirst(REQUEST_ID_HEADER_NAME); Assert.notNull(requestId, "No request-id header"); - WiretapClientHttpResponse wrapped = new WiretapClientHttpResponse(response); - ExchangeResult result = new ExchangeResult(request, wrapped); - this.capturedExchanges.put(requestId, result); - return wrapped; + WiretapClientHttpResponse wrappedResponse = new WiretapClientHttpResponse(response); + ExchangeResult result = new ExchangeResult(wrappedRequest, wrappedResponse); + this.exchanges.put(requestId, result); + return wrappedResponse; }); } /** - * Retrieve the request with the given "request-id" header. + * Retrieve the {@code ExchangeResult} for the given "request-id" header value. */ public ExchangeResult claimRequest(String requestId) { - ExchangeResult result = this.capturedExchanges.get(requestId); + ExchangeResult result = this.exchanges.get(requestId); Assert.notNull(result, "No match for request with id [" + requestId + "]"); return result; }