Skip to content

Commit 96bc1f5

Browse files
committed
Add interceptors and converters to RestTestClient.Builder
Closes gh-35268
1 parent 2b1a815 commit 96bc1f5

File tree

5 files changed

+146
-43
lines changed

5 files changed

+146
-43
lines changed

spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,6 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
115115
this(httpHandlerBuilder, null, sslInfo);
116116
}
117117

118-
/** Use given connector. */
119-
DefaultWebTestClientBuilder(ClientHttpConnector connector) {
120-
this(null, connector, null);
121-
}
122-
123118
private DefaultWebTestClientBuilder(@Nullable WebHttpHandlerBuilder httpHandlerBuilder,
124119
@Nullable ClientHttpConnector connector, @Nullable SslInfo sslInfo) {
125120

spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ static Builder bindToServer() {
242242
* @since 5.0.2
243243
*/
244244
static Builder bindToServer(ClientHttpConnector connector) {
245-
return new DefaultWebTestClientBuilder(connector);
245+
return new DefaultWebTestClientBuilder().clientConnector(connector);
246246
}
247247

248248

@@ -467,33 +467,6 @@ interface Builder {
467467
*/
468468
Builder filters(Consumer<List<ExchangeFilterFunction>> filtersConsumer);
469469

470-
/**
471-
* Configure an {@code EntityExchangeResult} callback that is invoked
472-
* every time after a response is fully decoded to a single entity, to a
473-
* List of entities, or to a byte[]. In effect, equivalent to each and
474-
* all of the below but registered once, globally:
475-
* <pre>
476-
* client.get().uri("/accounts/1")
477-
* .exchange()
478-
* .expectBody(Person.class).consumeWith(exchangeResult -&gt; ... ));
479-
*
480-
* client.get().uri("/accounts")
481-
* .exchange()
482-
* .expectBodyList(Person.class).consumeWith(exchangeResult -&gt; ... ));
483-
*
484-
* client.get().uri("/accounts/1")
485-
* .exchange()
486-
* .expectBody().consumeWith(exchangeResult -&gt; ... ));
487-
* </pre>
488-
* <p>Note that the configured consumer does not apply to responses
489-
* decoded to {@code Flux<T>} which can be consumed outside the workflow
490-
* of the test client, for example via {@code reactor.test.StepVerifier}.
491-
* @param consumer the consumer to apply to entity responses
492-
* @return the builder
493-
* @since 5.3.5
494-
*/
495-
Builder entityExchangeResultConsumer(Consumer<EntityExchangeResult<?>> consumer);
496-
497470
/**
498471
* Configure the codecs for the {@code WebClient} in the
499472
* {@link #exchangeStrategies(ExchangeStrategies) underlying}
@@ -533,6 +506,33 @@ interface Builder {
533506
*/
534507
Builder clientConnector(ClientHttpConnector connector);
535508

509+
/**
510+
* Configure an {@code EntityExchangeResult} callback that is invoked
511+
* every time after a response is fully decoded to a single entity, to a
512+
* List of entities, or to a byte[]. In effect, equivalent to each and
513+
* all of the below but registered once, globally:
514+
* <pre>
515+
* client.get().uri("/accounts/1")
516+
* .exchange()
517+
* .expectBody(Person.class).consumeWith(exchangeResult -&gt; ... ));
518+
*
519+
* client.get().uri("/accounts")
520+
* .exchange()
521+
* .expectBodyList(Person.class).consumeWith(exchangeResult -&gt; ... ));
522+
*
523+
* client.get().uri("/accounts/1")
524+
* .exchange()
525+
* .expectBody().consumeWith(exchangeResult -&gt; ... ));
526+
* </pre>
527+
* <p>Note that the configured consumer does not apply to responses
528+
* decoded to {@code Flux<T>} which can be consumed outside the workflow
529+
* of the test client, for example via {@code reactor.test.StepVerifier}.
530+
* @param consumer the consumer to apply to entity responses
531+
* @return the builder
532+
* @since 5.3.5
533+
*/
534+
Builder entityExchangeResultConsumer(Consumer<EntityExchangeResult<?>> consumer);
535+
536536
/**
537537
* Apply the given configurer to this builder instance.
538538
* <p>This can be useful for applying pre-packaged customizations.

spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClient.java

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,20 @@ class DefaultRestTestClient implements RestTestClient {
5656

5757
private final RestClient restClient;
5858

59+
private final Consumer<EntityExchangeResult<?>> entityResultConsumer;
60+
61+
private final DefaultRestTestClientBuilder<?> restTestClientBuilder;
62+
5963
private final AtomicLong requestIndex = new AtomicLong();
6064

6165

62-
DefaultRestTestClient(RestClient.Builder builder) {
66+
DefaultRestTestClient(
67+
RestClient.Builder builder, Consumer<EntityExchangeResult<?>> entityResultConsumer,
68+
DefaultRestTestClientBuilder<?> restTestClientBuilder) {
69+
6370
this.restClient = builder.build();
71+
this.entityResultConsumer = entityResultConsumer;
72+
this.restTestClientBuilder = restTestClientBuilder;
6473
}
6574

6675

@@ -108,9 +117,10 @@ private RequestBodyUriSpec methodInternal(HttpMethod httpMethod) {
108117
return new DefaultRequestBodyUriSpec(this.restClient.method(httpMethod));
109118
}
110119

120+
@SuppressWarnings("unchecked")
111121
@Override
112122
public <B extends Builder<B>> Builder<B> mutate() {
113-
return new DefaultRestTestClientBuilder<>(this.restClient.mutate());
123+
return (Builder<B>) this.restTestClientBuilder;
114124
}
115125

116126

@@ -242,7 +252,8 @@ public RequestHeadersSpec<?> body(Object body) {
242252
public ResponseSpec exchange() {
243253
return new DefaultResponseSpec(
244254
this.requestHeadersUriSpec.exchangeForRequiredValue(
245-
(request, response) -> new ExchangeResult(request, response, this.uriTemplate), false));
255+
(request, response) -> new ExchangeResult(request, response, this.uriTemplate), false),
256+
DefaultRestTestClient.this.entityResultConsumer);
246257
}
247258
}
248259

@@ -251,8 +262,11 @@ private static class DefaultResponseSpec implements ResponseSpec {
251262

252263
private final ExchangeResult exchangeResult;
253264

254-
DefaultResponseSpec(ExchangeResult result) {
265+
private final Consumer<EntityExchangeResult<?>> entityResultConsumer;
266+
267+
DefaultResponseSpec(ExchangeResult result, Consumer<EntityExchangeResult<?>> entityResultConsumer) {
255268
this.exchangeResult = result;
269+
this.entityResultConsumer = entityResultConsumer;
256270
}
257271

258272
@Override
@@ -280,25 +294,31 @@ public CookieAssertions expectCookie() {
280294
@Override
281295
public <B> BodySpec<B, ?> expectBody(ParameterizedTypeReference<B> bodyType) {
282296
B body = this.exchangeResult.getBody(bodyType);
283-
EntityExchangeResult<B> result = new EntityExchangeResult<>(this.exchangeResult, body);
297+
EntityExchangeResult<B> result = initExchangeResult(body);
284298
return new DefaultBodySpec<>(result);
285299
}
286300

287301
@Override
288302
public BodyContentSpec expectBody() {
289303
byte[] body = this.exchangeResult.getBody(byte[].class);
290-
EntityExchangeResult<byte[]> result = new EntityExchangeResult<>(this.exchangeResult, body);
304+
EntityExchangeResult<byte[]> result = initExchangeResult(body);
291305
return new DefaultBodyContentSpec(result);
292306
}
293307

294308
@Override
295309
public <T> EntityExchangeResult<T> returnResult(Class<T> elementClass) {
296-
return new EntityExchangeResult<>(this.exchangeResult, this.exchangeResult.getBody(elementClass));
310+
return initExchangeResult(this.exchangeResult.getBody(elementClass));
297311
}
298312

299313
@Override
300314
public <T> EntityExchangeResult<T> returnResult(ParameterizedTypeReference<T> elementTypeRef) {
301-
return new EntityExchangeResult<>(this.exchangeResult, this.exchangeResult.getBody(elementTypeRef));
315+
return initExchangeResult(this.exchangeResult.getBody(elementTypeRef));
316+
}
317+
318+
private <B> EntityExchangeResult<B> initExchangeResult(@Nullable B body) {
319+
EntityExchangeResult<B> result = new EntityExchangeResult<>(this.exchangeResult, body);
320+
result.assertWithDiagnostics(() -> this.entityResultConsumer.accept(result));
321+
return result;
302322
}
303323

304324
@Override

spring-test/src/main/java/org/springframework/test/web/servlet/client/DefaultRestTestClientBuilder.java

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616

1717
package org.springframework.test.web.servlet.client;
1818

19+
import java.util.List;
1920
import java.util.function.Consumer;
2021

2122
import org.springframework.http.HttpHeaders;
2223
import org.springframework.http.client.ClientHttpRequestFactory;
24+
import org.springframework.http.client.ClientHttpRequestInterceptor;
25+
import org.springframework.http.converter.HttpMessageConverters;
2326
import org.springframework.test.web.servlet.MockMvc;
2427
import org.springframework.test.web.servlet.MockMvcBuilder;
2528
import org.springframework.test.web.servlet.client.RestTestClient.MockMvcSetupBuilder;
@@ -30,6 +33,7 @@
3033
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
3134
import org.springframework.test.web.servlet.setup.RouterFunctionMockMvcBuilder;
3235
import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder;
36+
import org.springframework.util.Assert;
3337
import org.springframework.util.MultiValueMap;
3438
import org.springframework.web.client.ApiVersionInserter;
3539
import org.springframework.web.client.RestClient;
@@ -49,15 +53,22 @@ class DefaultRestTestClientBuilder<B extends RestTestClient.Builder<B>> implemen
4953

5054
private final RestClient.Builder restClientBuilder;
5155

56+
private Consumer<EntityExchangeResult<?>> entityResultConsumer = result -> {};
57+
5258

5359
DefaultRestTestClientBuilder() {
54-
this.restClientBuilder = RestClient.builder();
60+
this(RestClient.builder());
5561
}
5662

5763
DefaultRestTestClientBuilder(RestClient.Builder restClientBuilder) {
5864
this.restClientBuilder = restClientBuilder;
5965
}
6066

67+
DefaultRestTestClientBuilder(DefaultRestTestClientBuilder<B> other) {
68+
this.restClientBuilder = other.restClientBuilder.clone();
69+
this.entityResultConsumer = other.entityResultConsumer;
70+
}
71+
6172

6273
@Override
6374
public <T extends B> T baseUrl(String baseUrl) {
@@ -107,6 +118,31 @@ public <T extends B> T apiVersionInserter(ApiVersionInserter apiVersionInserter)
107118
return self();
108119
}
109120

121+
@Override
122+
public <T extends B> T requestInterceptor(ClientHttpRequestInterceptor interceptor) {
123+
this.restClientBuilder.requestInterceptor(interceptor);
124+
return self();
125+
}
126+
127+
@Override
128+
public <T extends B> T requestInterceptors(Consumer<List<ClientHttpRequestInterceptor>> interceptorsConsumer) {
129+
this.restClientBuilder.requestInterceptors(interceptorsConsumer);
130+
return self();
131+
}
132+
133+
@Override
134+
public <T extends B> T configureMessageConverters(Consumer<HttpMessageConverters.ClientBuilder> configurer) {
135+
this.restClientBuilder.configureMessageConverters(configurer);
136+
return self();
137+
}
138+
139+
@Override
140+
public <T extends B> T entityExchangeResultConsumer(Consumer<EntityExchangeResult<?>> entityResultConsumer) {
141+
Assert.notNull(entityResultConsumer, "'entityResultConsumer' is required");
142+
this.entityResultConsumer = this.entityResultConsumer.andThen(entityResultConsumer);
143+
return self();
144+
}
145+
110146
@SuppressWarnings("unchecked")
111147
protected <T extends B> T self() {
112148
return (T) this;
@@ -118,7 +154,8 @@ protected void setClientHttpRequestFactory(ClientHttpRequestFactory requestFacto
118154

119155
@Override
120156
public RestTestClient build() {
121-
return new DefaultRestTestClient(this.restClientBuilder);
157+
return new DefaultRestTestClient(
158+
this.restClientBuilder, this.entityResultConsumer, new DefaultRestTestClientBuilder<>(this));
122159
}
123160

124161

spring-test/src/main/java/org/springframework/test/web/servlet/client/RestTestClient.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.net.URI;
2020
import java.nio.charset.Charset;
2121
import java.time.ZonedDateTime;
22+
import java.util.List;
2223
import java.util.Map;
2324
import java.util.function.Consumer;
2425
import java.util.function.Function;
@@ -31,6 +32,8 @@
3132
import org.springframework.http.HttpMethod;
3233
import org.springframework.http.MediaType;
3334
import org.springframework.http.client.ClientHttpRequestFactory;
35+
import org.springframework.http.client.ClientHttpRequestInterceptor;
36+
import org.springframework.http.converter.HttpMessageConverters;
3437
import org.springframework.test.json.JsonComparator;
3538
import org.springframework.test.json.JsonCompareMode;
3639
import org.springframework.test.json.JsonComparison;
@@ -261,6 +264,54 @@ interface Builder<B extends Builder<B>> {
261264
*/
262265
<T extends B> T apiVersionInserter(ApiVersionInserter apiVersionInserter);
263266

267+
/**
268+
* Add the given request interceptor to the end of the interceptor chain.
269+
* @param interceptor the interceptor to be added to the chain
270+
*/
271+
<T extends B> T requestInterceptor(ClientHttpRequestInterceptor interceptor);
272+
273+
/**
274+
* Manipulate the interceptors with the given consumer. The list provided to
275+
* the consumer is "live", so that the consumer can be used to remove
276+
* interceptors, change ordering, etc.
277+
* @param interceptorsConsumer a function that consumes the interceptors list
278+
* @return this builder
279+
*/
280+
<T extends B> T requestInterceptors(Consumer<List<ClientHttpRequestInterceptor>> interceptorsConsumer);
281+
282+
/**
283+
* Configure the message converters to use for the request and response body.
284+
* @param configurer the configurer to apply on an empty {@link HttpMessageConverters.ClientBuilder}.
285+
* @return this builder
286+
*/
287+
<T extends B> T configureMessageConverters(Consumer<HttpMessageConverters.ClientBuilder> configurer);
288+
289+
/**
290+
* Configure an {@code EntityExchangeResult} callback that is invoked
291+
* every time after a response is fully decoded to a single entity, to a
292+
* List of entities, or to a byte[]. In effect, equivalent to each and
293+
* all of the below but registered once, globally:
294+
* <pre>
295+
* client.get().uri("/accounts/1")
296+
* .exchange()
297+
* .expectBody(Person.class).consumeWith(exchangeResult -&gt; ... ));
298+
*
299+
* client.get().uri("/accounts")
300+
* .exchange()
301+
* .expectBodyList(Person.class).consumeWith(exchangeResult -&gt; ... ));
302+
*
303+
* client.get().uri("/accounts/1")
304+
* .exchange()
305+
* .expectBody().consumeWith(exchangeResult -&gt; ... ));
306+
* </pre>
307+
* <p>Note that the configured consumer does not apply to responses
308+
* decoded to {@code Flux<T>} which can be consumed outside the workflow
309+
* of the test client, for example via {@code reactor.test.StepVerifier}.
310+
* @param consumer the consumer to apply to entity responses
311+
* @return the builder
312+
*/
313+
<T extends B> T entityExchangeResultConsumer(Consumer<EntityExchangeResult<?>> consumer);
314+
264315
/**
265316
* Build the {@link RestTestClient} instance.
266317
*/

0 commit comments

Comments
 (0)