This commit is contained in:
Rossen Stoyanchev 2017-02-20 21:59:47 -05:00
parent 2515134f8e
commit 262e5f783d
8 changed files with 114 additions and 143 deletions

View File

@ -300,10 +300,8 @@ class DefaultWebTestClient implements WebTestClient {
}
public EntityExchangeResult<Void> 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<ExchangeResult> 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 <T> EntityExchangeResult<T> 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 <T> EntityExchangeResult<List<T>> isEqualTo(List<T> 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<Object> 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<Object> 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 <K, V> EntityExchangeResult<Map<K, V>> isEqualTo(Map<K, V> 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;
}

View File

@ -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}.
*
* <p>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.
* <p>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.
*
* <p>Raw request and response content may also be accessed once complete via
* {@link #getRequestContent()} or {@link #getResponseContent()}.
* <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
* @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> T assertWithDiagnosticsAndReturn(Supplier<T> 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<byte[]> body) {
private String formatBody(MediaType contentType, MonoProcessor<byte[]> body) {
if (body.isSuccess()) {
byte[] bytes = body.blockMillis(0);
if (bytes.length == 0) {

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<T>} 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<T>} where {@code <T>} is the
* expected element type. The returned {@code Flux} may for example be
* verified with the Reactor {@code StepVerifier}.
*/
<T> FluxExchangeResult<T> returnResult();
}
@ -530,7 +531,7 @@ public interface WebTestClient {
<T> EntityExchangeResult<T> 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.
*/
<T> EntityExchangeResult<T> 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.
*/
<T> EntityExchangeResult<List<T>> 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.
*/
<K, V> EntityExchangeResult<Map<K, V>> returnResult();

View File

@ -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<byte[]> getBodyContent() {
return this.body;
@ -60,39 +61,39 @@ class WiretapClientHttpRequest extends ClientHttpRequestDecorator {
public Mono<Void> writeWith(Publisher<? extends DataBuffer> 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<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> 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<Void> 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);

View File

@ -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<byte[]> 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);

View File

@ -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<String, ExchangeResult> capturedExchanges = new ConcurrentHashMap<>();
private final Map<String, ExchangeResult> 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;
}