Skip to content

Commit 9829a62

Browse files
committed
Refactor WebTestClient assertions take 3
WebTestClient now defines all the steps from setup to performing an exchange and applying expectations. The order of expectations now ensures the response status and headers are verified first since that's available before the body is consumed and also because it determines how the body is to be decoded, i.e. error vs success scenarios. There is now a built-in option for verifying the response as a Map along with Map-specific assertions. There are similar options for verifying the response as a List as well as whether to "collect" the list or "take" the first N elements from the response stream.
1 parent f1653cc commit 9829a62

File tree

16 files changed

+586
-408
lines changed

16 files changed

+586
-408
lines changed

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

Lines changed: 263 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
import java.nio.charset.Charset;
2020
import java.time.Duration;
2121
import java.time.ZonedDateTime;
22+
import java.util.Arrays;
2223
import java.util.List;
2324
import java.util.Map;
2425
import java.util.Optional;
2526
import java.util.concurrent.ConcurrentHashMap;
2627
import java.util.concurrent.atomic.AtomicLong;
2728
import java.util.function.Consumer;
2829
import java.util.function.Function;
30+
import java.util.stream.Collectors;
2931

3032
import org.reactivestreams.Publisher;
3133
import reactor.core.publisher.Flux;
@@ -34,10 +36,10 @@
3436
import org.springframework.core.ResolvableType;
3537
import org.springframework.core.io.buffer.DataBuffer;
3638
import org.springframework.http.HttpHeaders;
39+
import org.springframework.http.HttpMethod;
3740
import org.springframework.http.MediaType;
3841
import org.springframework.http.client.reactive.ClientHttpConnector;
3942
import org.springframework.http.client.reactive.ClientHttpRequest;
40-
import org.springframework.test.util.AssertionErrors;
4143
import org.springframework.util.Assert;
4244
import org.springframework.util.MultiValueMap;
4345
import org.springframework.web.reactive.function.BodyInserter;
@@ -46,6 +48,9 @@
4648
import org.springframework.web.reactive.function.client.WebClient;
4749
import org.springframework.web.util.UriBuilder;
4850

51+
import static org.springframework.test.util.AssertionErrors.assertEquals;
52+
import static org.springframework.test.util.AssertionErrors.assertTrue;
53+
import static org.springframework.test.util.AssertionErrors.fail;
4954
import static org.springframework.web.reactive.function.BodyExtractors.toDataBuffers;
5055
import static org.springframework.web.reactive.function.BodyExtractors.toFlux;
5156
import static org.springframework.web.reactive.function.BodyExtractors.toMono;
@@ -242,88 +247,301 @@ public DefaultHeaderSpec ifNoneMatch(String... ifNoneMatches) {
242247

243248
@Override
244249
public ResponseSpec exchange() {
245-
return new DefaultResponseSpec(this.requestId, this.headerSpec.exchange());
250+
return createResponseSpec(this.headerSpec.exchange());
246251
}
247252

248253
@Override
249254
public <T> ResponseSpec exchange(BodyInserter<T, ? super ClientHttpRequest> inserter) {
250-
return new DefaultResponseSpec(this.requestId, this.headerSpec.exchange(inserter));
255+
return createResponseSpec(this.headerSpec.exchange(inserter));
251256
}
252257

253258
@Override
254259
public <T, S extends Publisher<T>> ResponseSpec exchange(S publisher, Class<T> elementClass) {
255-
return new DefaultResponseSpec(this.requestId, this.headerSpec.exchange(publisher, elementClass));
260+
return createResponseSpec(this.headerSpec.exchange(publisher, elementClass));
256261
}
262+
263+
protected DefaultResponseSpec createResponseSpec(Mono<ClientResponse> responseMono) {
264+
ClientResponse response = responseMono.block(getTimeout());
265+
WiretapConnector.Info info = connectorListener.retrieveRequest(this.requestId);
266+
HttpMethod method = info.getMethod();
267+
URI url = info.getUrl();
268+
HttpHeaders headers = info.getRequestHeaders();
269+
ExchangeResult<Flux<DataBuffer>> result = ExchangeResult.fromResponse(method, url, headers, response);
270+
return new DefaultResponseSpec(result, response);
271+
}
272+
257273
}
258274

259-
private class DefaultResponseSpec implements ResponseSpec {
275+
private abstract class ResponseSpecSupport {
260276

261-
private final String requestId;
277+
private final ExchangeResult<Flux<DataBuffer>> exchangeResult;
278+
279+
private final ClientResponse response;
280+
281+
282+
public ResponseSpecSupport(ExchangeResult<Flux<DataBuffer>> result, ClientResponse response) {
283+
this.exchangeResult = result;
284+
this.response = response;
285+
}
262286

263-
private final Mono<ClientResponse> responseMono;
287+
public ResponseSpecSupport(ResponseSpecSupport responseSpec) {
288+
this.exchangeResult = responseSpec.getExchangeResult();
289+
this.response = responseSpec.getResponse();
290+
}
291+
292+
293+
protected ExchangeResult<Flux<DataBuffer>> getExchangeResult() {
294+
return this.exchangeResult;
295+
}
264296

297+
protected ClientResponse getResponse() {
298+
return this.response;
299+
}
300+
301+
protected HttpHeaders getResponseHeaders() {
302+
return getExchangeResult().getResponseHeaders();
303+
}
265304

266-
public DefaultResponseSpec(String requestId, Mono<ClientResponse> responseMono) {
267-
this.requestId = requestId;
268-
this.responseMono = responseMono;
305+
protected <T> ExchangeResult<T> createResultWithDecodedBody(T body) {
306+
return ExchangeResult.withDecodedBody(this.exchangeResult, body);
307+
}
308+
309+
}
310+
311+
private class DefaultResponseSpec extends ResponseSpecSupport implements ResponseSpec {
312+
313+
314+
public DefaultResponseSpec(ExchangeResult<Flux<DataBuffer>> result, ClientResponse response) {
315+
super(result, response);
269316
}
270317

271318

272319
@Override
273-
public <T> ExchangeResult<T> decodeEntity(Class<T> entityClass) {
274-
return decodeEntity(ResolvableType.forClass(entityClass));
320+
public StatusAssertions expectStatus() {
321+
return new StatusAssertions(getResponse().statusCode(), this);
275322
}
276323

277324
@Override
278-
public <T> ExchangeResult<List<T>> decodeAndCollect(Class<T> elementClass) {
279-
return decodeAndCollect(ResolvableType.forClass(elementClass));
325+
public HeaderAssertions expectHeader() {
326+
return new HeaderAssertions(getResponseHeaders(), this);
280327
}
281328

282329
@Override
283-
public <T> ExchangeResult<Flux<T>> decodeFlux(Class<T> elementClass) {
284-
return decodeFlux(ResolvableType.forClass(elementClass));
330+
public BodySpec expectBody() {
331+
return new DefaultBodySpec(this);
285332
}
286333

287334
@Override
288-
public <T> ExchangeResult<T> decodeEntity(ResolvableType elementType) {
289-
return this.responseMono.then(response -> {
290-
Mono<T> entityMono = response.body(toMono(elementType));
291-
return entityMono.map(entity -> createTestExchange(entity, response));
292-
}).block(getTimeout());
335+
public ElementBodySpec expectBody(Class<?> elementType) {
336+
return expectBody(ResolvableType.forClass(elementType));
293337
}
294338

295339
@Override
296-
public <T> ExchangeResult<List<T>> decodeAndCollect(ResolvableType elementType) {
297-
return this.responseMono.then(response -> {
298-
Flux<T> entityFlux = response.body(toFlux(elementType));
299-
return entityFlux.collectList().map(list -> createTestExchange(list, response));
300-
}).block(getTimeout());
340+
public ElementBodySpec expectBody(ResolvableType elementType) {
341+
return new DefaultElementBodySpec(this, elementType);
301342
}
302343

303344
@Override
304-
public <T> ExchangeResult<Flux<T>> decodeFlux(ResolvableType elementType) {
305-
return this.responseMono.map(response -> {
306-
Flux<T> entityFlux = response.body(toFlux(elementType));
307-
return createTestExchange(entityFlux, response);
308-
}).block(getTimeout());
345+
public ResponseSpec consumeWith(Consumer<ExchangeResult<Flux<DataBuffer>>> consumer) {
346+
consumer.accept(getExchangeResult());
347+
return this;
309348
}
310349

311350
@Override
312-
public ExchangeResult<Void> expectNoBody() {
313-
return this.responseMono.map(response -> {
314-
DataBuffer buffer = response.body(toDataBuffers()).blockFirst(getTimeout());
315-
AssertionErrors.assertTrue("Expected empty body", buffer == null);
316-
ExchangeResult<Void> exchange = createTestExchange(null, response);
317-
return exchange;
318-
}).block(getTimeout());
351+
public ExchangeResult<Flux<DataBuffer>> returnResult() {
352+
return getExchangeResult();
353+
}
354+
}
355+
356+
private class DefaultBodySpec extends ResponseSpecSupport implements BodySpec {
357+
358+
359+
public DefaultBodySpec(ResponseSpecSupport responseSpec) {
360+
super(responseSpec);
361+
}
362+
363+
364+
@Override
365+
public ExchangeResult<Void> isEmpty() {
366+
DataBuffer buffer = getResponse().body(toDataBuffers()).blockFirst(getTimeout());
367+
assertTrue("Expected empty body", buffer == null);
368+
return createResultWithDecodedBody(null);
319369
}
320370

321-
private <T> ExchangeResult<T> createTestExchange(T body, ClientResponse response) {
322-
WiretapConnector.Info wiretapInfo = connectorListener.retrieveRequest(requestId);
323-
ClientHttpRequest request = wiretapInfo.getRequest();
324-
return new ExchangeResult<T>(
325-
request.getMethod(), request.getURI(), request.getHeaders(),
326-
response.statusCode(), response.headers().asHttpHeaders(), body);
371+
@Override
372+
public MapBodySpec map(Class<?> keyType, Class<?> valueType) {
373+
return map(ResolvableType.forClass(keyType), ResolvableType.forClass(valueType));
374+
}
375+
376+
@Override
377+
public MapBodySpec map(ResolvableType keyType, ResolvableType valueType) {
378+
return new DefaultMapBodySpec(this, keyType, valueType);
379+
}
380+
}
381+
382+
private class DefaultMapBodySpec extends ResponseSpecSupport implements MapBodySpec {
383+
384+
private final Map<?, ?> body;
385+
386+
387+
public DefaultMapBodySpec(ResponseSpecSupport spec, ResolvableType keyType, ResolvableType valueType) {
388+
super(spec);
389+
ResolvableType mapType = ResolvableType.forClassWithGenerics(Map.class, keyType, valueType);
390+
this.body = (Map<?, ?>) spec.getResponse().body(toMono(mapType)).block(getTimeout());
391+
}
392+
393+
394+
@Override
395+
public <K, V> ExchangeResult<Map<K, V>> isEqualTo(Map<K, V> expected) {
396+
return returnResult();
397+
}
398+
399+
@Override
400+
public MapBodySpec hasSize(int size) {
401+
assertEquals("Response body map size", size, this.body.size());
402+
return this;
403+
}
404+
405+
@Override
406+
public MapBodySpec contains(Object key, Object value) {
407+
assertEquals("Response body map value for key " + key, value, this.body.get(key));
408+
return this;
409+
}
410+
411+
@Override
412+
public MapBodySpec containsKeys(Object... keys) {
413+
List<Object> missing = Arrays.stream(keys)
414+
.filter(key -> !this.body.containsKey(key))
415+
.collect(Collectors.toList());
416+
if (!missing.isEmpty()) {
417+
fail("Response body map does not contain keys " + Arrays.toString(keys));
418+
}
419+
return this;
420+
}
421+
422+
@Override
423+
public MapBodySpec containsValues(Object... values) {
424+
List<Object> missing = Arrays.stream(values)
425+
.filter(value -> !this.body.containsValue(value))
426+
.collect(Collectors.toList());
427+
if (!missing.isEmpty()) {
428+
fail("Response body map does not contain values " + Arrays.toString(values));
429+
}
430+
return this;
431+
}
432+
433+
@Override
434+
@SuppressWarnings("unchecked")
435+
public <K, V> ExchangeResult<Map<K, V>> returnResult() {
436+
return createResultWithDecodedBody((Map<K, V>) this.body);
437+
}
438+
}
439+
440+
private class DefaultElementBodySpec extends ResponseSpecSupport implements ElementBodySpec {
441+
442+
private final ResolvableType elementType;
443+
444+
445+
public DefaultElementBodySpec(ResponseSpecSupport spec, ResolvableType elementType) {
446+
super(spec);
447+
this.elementType = elementType;
448+
}
449+
450+
451+
@Override
452+
public SingleValueBodySpec value() {
453+
return new DefaultSingleValueBodySpec(this, this.elementType);
454+
}
455+
456+
@Override
457+
public ListBodySpec list() {
458+
return new DefaultListBodySpec(this, this.elementType, -1);
459+
}
460+
461+
@Override
462+
public ListBodySpec list(int elementCount) {
463+
return new DefaultListBodySpec(this, this.elementType, elementCount);
464+
}
465+
466+
@Override
467+
public <T> ExchangeResult<Flux<T>> returnResult() {
468+
Flux<T> flux = getResponse().body(toFlux(this.elementType));
469+
return createResultWithDecodedBody(flux);
470+
}
471+
}
472+
473+
private class DefaultSingleValueBodySpec extends ResponseSpecSupport
474+
implements SingleValueBodySpec {
475+
476+
private final Object body;
477+
478+
479+
public DefaultSingleValueBodySpec(ResponseSpecSupport spec, ResolvableType elementType) {
480+
super(spec);
481+
this.body = getResponse().body(toMono(elementType)).block(getTimeout());
482+
}
483+
484+
485+
@Override
486+
public <T> ExchangeResult<T> isEqualTo(Object expected) {
487+
assertEquals("Response body", expected, this.body);
488+
return returnResult();
489+
}
490+
491+
@Override
492+
@SuppressWarnings("unchecked")
493+
public <T> ExchangeResult<T> returnResult() {
494+
return createResultWithDecodedBody((T) this.body);
495+
}
496+
}
497+
498+
private class DefaultListBodySpec extends ResponseSpecSupport
499+
implements ListBodySpec {
500+
501+
private final List<?> body;
502+
503+
504+
public DefaultListBodySpec(ResponseSpecSupport spec, ResolvableType elementType, int elementCount) {
505+
super(spec);
506+
Flux<?> flux = getResponse().body(toFlux(elementType));
507+
if (elementCount >= 0) {
508+
flux = flux.take(elementCount);
509+
}
510+
this.body = flux.collectList().block(getTimeout());
511+
}
512+
513+
514+
@Override
515+
public <T> ExchangeResult<List<T>> isEqualTo(List<T> expected) {
516+
assertEquals("Response body", expected, this.body);
517+
return returnResult();
518+
}
519+
520+
@Override
521+
public ListBodySpec hasSize(int size) {
522+
return this;
523+
}
524+
525+
@Override
526+
public ListBodySpec contains(Object... elements) {
527+
List<Object> elementList = Arrays.asList(elements);
528+
String message = "Response body does not contain " + elementList;
529+
assertTrue(message, this.body.containsAll(elementList));
530+
return this;
531+
}
532+
533+
@Override
534+
public ListBodySpec doesNotContain(Object... elements) {
535+
List<Object> elementList = Arrays.asList(elements);
536+
String message = "Response body should have contained " + elementList;
537+
assertTrue(message, !this.body.containsAll(Arrays.asList(elements)));
538+
return this;
539+
}
540+
541+
@Override
542+
@SuppressWarnings("unchecked")
543+
public <T> ExchangeResult<List<T>> returnResult() {
544+
return createResultWithDecodedBody((List<T>) this.body);
327545
}
328546
}
329547

@@ -346,7 +564,7 @@ public String registerRequestId(WebClient.HeaderSpec headerSpec) {
346564

347565
@Override
348566
public void accept(WiretapConnector.Info info) {
349-
Optional.ofNullable(info.getRequest().getHeaders().getFirst(REQUEST_ID_HEADER_NAME))
567+
Optional.ofNullable(info.getRequestHeaders().getFirst(REQUEST_ID_HEADER_NAME))
350568
.ifPresent(id -> this.exchanges.put(id, info));
351569
}
352570

0 commit comments

Comments
 (0)