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 c0fd64bb8ed5..8d0ec7de0c5d 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -79,6 +79,8 @@ class DefaultWebTestClient implements WebTestClient { @Nullable private final MultiValueMap defaultCookies; + private final Consumer> entityResultConsumer; + private final Duration responseTimeout; private final DefaultWebTestClientBuilder builder; @@ -89,6 +91,7 @@ class DefaultWebTestClient implements WebTestClient { DefaultWebTestClient(ClientHttpConnector connector, Function exchangeFactory, UriBuilderFactory uriBuilderFactory, @Nullable HttpHeaders headers, @Nullable MultiValueMap cookies, + Consumer> entityResultConsumer, @Nullable Duration responseTimeout, DefaultWebTestClientBuilder clientBuilder) { this.wiretapConnector = new WiretapConnector(connector); @@ -96,6 +99,7 @@ class DefaultWebTestClient implements WebTestClient { this.uriBuilderFactory = uriBuilderFactory; this.defaultHeaders = headers; this.defaultCookies = cookies; + this.entityResultConsumer = entityResultConsumer; this.responseTimeout = (responseTimeout != null ? responseTimeout : Duration.ofSeconds(5)); this.builder = clientBuilder; } @@ -357,7 +361,8 @@ public ResponseSpec exchange() { ExchangeResult result = wiretapConnector.getExchangeResult( this.requestId, this.uriTemplate, getResponseTimeout()); - return new DefaultResponseSpec(result, response, getResponseTimeout()); + return new DefaultResponseSpec(result, response, + DefaultWebTestClient.this.entityResultConsumer, getResponseTimeout()); } private ClientRequest.Builder initRequestBuilder() { @@ -408,12 +413,19 @@ private static class DefaultResponseSpec implements ResponseSpec { private final ClientResponse response; + private final Consumer> entityResultConsumer; + private final Duration timeout; - DefaultResponseSpec(ExchangeResult exchangeResult, ClientResponse response, Duration timeout) { + DefaultResponseSpec( + ExchangeResult exchangeResult, ClientResponse response, + Consumer> entityResultConsumer, + Duration timeout) { + this.exchangeResult = exchangeResult; this.response = response; + this.entityResultConsumer = entityResultConsumer; this.timeout = timeout; } @@ -435,14 +447,14 @@ public CookieAssertions expectCookie() { @Override public BodySpec expectBody(Class bodyType) { B body = this.response.bodyToMono(bodyType).block(this.timeout); - EntityExchangeResult entityResult = new EntityExchangeResult<>(this.exchangeResult, body); + EntityExchangeResult entityResult = initEntityExchangeResult(body); return new DefaultBodySpec<>(entityResult); } @Override public BodySpec expectBody(ParameterizedTypeReference bodyType) { B body = this.response.bodyToMono(bodyType).block(this.timeout); - EntityExchangeResult entityResult = new EntityExchangeResult<>(this.exchangeResult, body); + EntityExchangeResult entityResult = initEntityExchangeResult(body); return new DefaultBodySpec<>(entityResult); } @@ -459,7 +471,7 @@ public ListBodySpec expectBodyList(ParameterizedTypeReference elementT private ListBodySpec getListBodySpec(Flux flux) { List body = flux.collectList().block(this.timeout); - EntityExchangeResult> entityResult = new EntityExchangeResult<>(this.exchangeResult, body); + EntityExchangeResult> entityResult = initEntityExchangeResult(body); return new DefaultListBodySpec<>(entityResult); } @@ -467,10 +479,16 @@ private ListBodySpec getListBodySpec(Flux flux) { public BodyContentSpec expectBody() { ByteArrayResource resource = this.response.bodyToMono(ByteArrayResource.class).block(this.timeout); byte[] body = (resource != null ? resource.getByteArray() : null); - EntityExchangeResult entityResult = new EntityExchangeResult<>(this.exchangeResult, body); + EntityExchangeResult entityResult = initEntityExchangeResult(body); return new DefaultBodyContentSpec(entityResult); } + private EntityExchangeResult initEntityExchangeResult(@Nullable B body) { + EntityExchangeResult result = new EntityExchangeResult<>(this.exchangeResult, body); + result.assertWithDiagnostics(() -> this.entityResultConsumer.accept(result)); + return result; + } + @Override public FluxExchangeResult returnResult(Class elementClass) { Flux body; diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java index 4a398e19a5be..f403ad913846 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java @@ -92,6 +92,8 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { @Nullable private List filters; + private Consumer> entityResultConsumer = result -> {}; + @Nullable private ExchangeStrategies strategies; @@ -149,6 +151,7 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { this.defaultCookies = (other.defaultCookies != null ? new LinkedMultiValueMap<>(other.defaultCookies) : null); this.filters = (other.filters != null ? new ArrayList<>(other.filters) : null); + this.entityResultConsumer = other.entityResultConsumer; this.strategies = other.strategies; this.strategiesConfigurers = (other.strategiesConfigurers != null ? new ArrayList<>(other.strategiesConfigurers) : null); @@ -207,7 +210,7 @@ private MultiValueMap initCookies() { @Override public WebTestClient.Builder filter(ExchangeFilterFunction filter) { - Assert.notNull(filter, "ExchangeFilterFunction must not be null"); + Assert.notNull(filter, "ExchangeFilterFunction is required"); initFilters().add(filter); return this; } @@ -225,6 +228,13 @@ private List initFilters() { return this.filters; } + @Override + public WebTestClient.Builder entityExchangeResultConsumer(Consumer> entityResultConsumer) { + Assert.notNull(entityResultConsumer, "`entityResultConsumer` is required"); + this.entityResultConsumer = this.entityResultConsumer.andThen(entityResultConsumer); + return this; + } + @Override public WebTestClient.Builder codecs(Consumer configurer) { if (this.strategiesConfigurers == null) { @@ -287,7 +297,7 @@ public WebTestClient build() { return new DefaultWebTestClient(connectorToUse, exchangeFactory, initUriBuilderFactory(), this.defaultHeaders != null ? HttpHeaders.readOnlyHttpHeaders(this.defaultHeaders) : null, this.defaultCookies != null ? CollectionUtils.unmodifiableMultiValueMap(this.defaultCookies) : null, - this.responseTimeout, new DefaultWebTestClientBuilder(this)); + this.entityResultConsumer, this.responseTimeout, new DefaultWebTestClientBuilder(this)); } private static ClientHttpConnector initConnector() { 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 f2436c80d32d..6d2fcefebd70 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -438,6 +438,33 @@ interface Builder { */ Builder filters(Consumer> filtersConsumer); + /** + * Configure an {@code EntityExchangeResult} callback that is invoked + * every time after a response is fully decoded to a single entity, to a + * List of entities, or to a byte[]. In effect, equivalent to each and + * all of the below but registered once, globally: + *
+		 * client.get().uri("/accounts/1")
+		 *         .exchange()
+		 *         .expectBody(Person.class).consumeWith(exchangeResult -> ... ));
+		 *
+		 * client.get().uri("/accounts")
+		 *         .exchange()
+		 *         .expectBodyList(Person.class).consumeWith(exchangeResult -> ... ));
+		 *
+		 * client.get().uri("/accounts/1")
+		 *         .exchange()
+		 *         .expectBody().consumeWith(exchangeResult -> ... ));
+		 * 
+ *

Note that the configured consumer does not apply to responses + * decoded to {@code Flux} which can be consumed outside the workflow + * of the test client, for example via {@code reactor.test.StepVerifier}. + * @param consumer the consumer to apply to entity responses + * @return the builder + * @since 5.3.5 + */ + Builder entityExchangeResultConsumer(Consumer> consumer); + /** * Configure the codecs for the {@code WebClient} in the * {@link #exchangeStrategies(ExchangeStrategies) underlying}