Refactor WebTestClient response body expectations

Reduce the number of required steps and re-introduce generics support
for simple Class<T> cases.
This commit is contained in:
Rossen Stoyanchev 2017-04-13 09:47:01 -04:00
parent bf3fe93dbd
commit 0e84f246cb
8 changed files with 203 additions and 353 deletions

View File

@ -47,13 +47,13 @@ import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriBuilder;
import static java.util.stream.Collectors.toList;
import static org.springframework.test.util.AssertionErrors.assertEquals;
import static org.springframework.test.util.AssertionErrors.assertTrue;
import static org.springframework.web.reactive.function.BodyExtractors.toDataBuffers;
import static org.springframework.web.reactive.function.BodyExtractors.toFlux;
import static org.springframework.web.reactive.function.BodyExtractors.toMono;
/**
* Default implementation of {@link WebTestClient}.
*
@ -290,63 +290,57 @@ class DefaultWebTestClient implements WebTestClient {
private DefaultResponseSpec toResponseSpec(Mono<ClientResponse> mono) {
ClientResponse clientResponse = mono.block(getTimeout());
ExchangeResult exchangeResult = wiretapConnector.claimRequest(this.requestId);
return new DefaultResponseSpec(exchangeResult, clientResponse);
return new DefaultResponseSpec(exchangeResult, clientResponse, getTimeout());
}
}
/**
* The {@code ExchangeResult} with live, undecoded {@link ClientResponse}.
*/
private class UndecodedExchangeResult extends ExchangeResult {
private static class UndecodedExchangeResult extends ExchangeResult {
private final ClientResponse response;
private final Duration timeout;
public UndecodedExchangeResult(ExchangeResult result, ClientResponse response) {
UndecodedExchangeResult(ExchangeResult result, ClientResponse response, Duration timeout) {
super(result);
this.response = response;
this.timeout = timeout;
}
public EntityExchangeResult<?> consumeSingle(ResolvableType elementType) {
Object body = this.response.body(toMono(elementType)).block(getTimeout());
return new EntityExchangeResult<>(this, body);
}
public EntityExchangeResult<List<?>> consumeList(ResolvableType elementType, int count) {
Flux<?> flux = this.response.body(toFlux(elementType));
if (count >= 0) {
flux = flux.take(count);
}
List<?> body = flux.collectList().block(getTimeout());
return new EntityExchangeResult<>(this, body);
}
public <T> FluxExchangeResult<T> decodeBody(ResolvableType elementType) {
Flux<T> body = this.response.body(toFlux(elementType));
return new FluxExchangeResult<>(this, body, getTimeout());
}
@SuppressWarnings("unchecked")
public EntityExchangeResult<Map<?, ?>> consumeMap(ResolvableType keyType, ResolvableType valueType) {
ResolvableType mapType = ResolvableType.forClassWithGenerics(Map.class, keyType, valueType);
return (EntityExchangeResult<Map<?, ?>>) consumeSingle(mapType);
public <T> EntityExchangeResult<T> decode(ResolvableType bodyType) {
T body = (T) this.response.body(toMono(bodyType)).block(this.timeout);
return new EntityExchangeResult<>(this, body);
}
public EntityExchangeResult<Void> consumeEmpty() {
DataBuffer buffer = this.response.body(toDataBuffers()).blockFirst(getTimeout());
public <T> EntityExchangeResult<List<T>> decodeToList(ResolvableType elementType) {
Flux<T> flux = this.response.body(toFlux(elementType));
List<T> body = flux.collectList().block(this.timeout);
return new EntityExchangeResult<>(this, body);
}
public <T> FluxExchangeResult<T> decodeToFlux(ResolvableType elementType) {
Flux<T> body = this.response.body(toFlux(elementType));
return new FluxExchangeResult<>(this, body, this.timeout);
}
public EntityExchangeResult<Void> decodeToEmpty() {
DataBuffer buffer = this.response.body(toDataBuffers()).blockFirst(this.timeout);
assertWithDiagnostics(() -> assertTrue("Expected empty body", buffer == null));
return new EntityExchangeResult<>(this, null);
}
}
private class DefaultResponseSpec implements ResponseSpec {
private static class DefaultResponseSpec implements ResponseSpec {
private final UndecodedExchangeResult result;
public DefaultResponseSpec(ExchangeResult result, ClientResponse response) {
this.result = new UndecodedExchangeResult(result, response);
DefaultResponseSpec(ExchangeResult result, ClientResponse response, Duration timeout) {
this.result = new UndecodedExchangeResult(result, response, timeout);
}
@Override
@ -360,210 +354,133 @@ class DefaultWebTestClient implements WebTestClient {
}
@Override
public TypeBodySpec expectBody(Class<?> elementType) {
return expectBody(ResolvableType.forClass(elementType));
public <B> BodySpec<B, ?> expectBody(Class<B> bodyType) {
return expectBody(ResolvableType.forClass(bodyType));
}
@Override
public TypeBodySpec expectBody(ResolvableType elementType) {
return new DefaultTypeBodySpec(this.result, elementType);
public <B> BodySpec<B, ?> expectBody(ResolvableType bodyType) {
return new DefaultBodySpec<>(this.result.decode(bodyType));
}
@Override
public BodySpec expectBody() {
return new DefaultBodySpec(this.result);
public <E> ListBodySpec<E> expectBodyList(Class<E> elementType) {
return expectBodyList(ResolvableType.forClass(elementType));
}
@Override
public <E> ListBodySpec<E> expectBodyList(ResolvableType elementType) {
return new DefaultListBodySpec<>(this.result.decodeToList(elementType));
}
@Override
public BodyContentSpec expectBody() {
return new DefaultBodyContentSpec(this.result);
}
@Override
public <T> FluxExchangeResult<T> returnResult(Class<T> elementType) {
return returnResult(ResolvableType.forClass(elementType));
}
@Override
public <T> FluxExchangeResult<T> returnResult(ResolvableType elementType) {
return this.result.decodeToFlux(elementType);
}
}
private class DefaultTypeBodySpec implements TypeBodySpec {
private static class DefaultBodySpec<B, S extends BodySpec<B, S>> implements BodySpec<B, S> {
private final EntityExchangeResult<B> result;
DefaultBodySpec(EntityExchangeResult<B> result) {
this.result = result;
}
protected EntityExchangeResult<B> getResult() {
return this.result;
}
@Override
public <T extends S> T isEqualTo(B expected) {
Object actual = this.result.getResponseBody();
this.result.assertWithDiagnostics(() -> assertEquals("Response body", expected, actual));
return self();
}
@SuppressWarnings("unchecked")
private <T extends S> T self() {
return (T) this;
}
@Override
public EntityExchangeResult<B> returnResult() {
return this.result;
}
}
private static class DefaultListBodySpec<E> extends DefaultBodySpec<List<E>, ListBodySpec<E>>
implements ListBodySpec<E> {
DefaultListBodySpec(EntityExchangeResult<List<E>> result) {
super(result);
}
@Override
public ListBodySpec<E> hasSize(int size) {
List<E> actual = getResult().getResponseBody();
String message = "Response body does not contain " + size + " elements";
getResult().assertWithDiagnostics(() -> assertEquals(message, size, actual.size()));
return this;
}
@Override
@SuppressWarnings("unchecked")
public ListBodySpec<E> contains(E... elements) {
List<E> expected = Arrays.asList(elements);
List<E> actual = getResult().getResponseBody();
String message = "Response body does not contain " + expected;
getResult().assertWithDiagnostics(() -> assertTrue(message, actual.containsAll(expected)));
return this;
}
@Override
@SuppressWarnings("unchecked")
public ListBodySpec<E> doesNotContain(E... elements) {
List<E> expected = Arrays.asList(elements);
List<E> actual = getResult().getResponseBody();
String message = "Response body should have contained " + expected;
getResult().assertWithDiagnostics(() -> assertTrue(message, !actual.containsAll(expected)));
return this;
}
@Override
@SuppressWarnings("unchecked")
public EntityExchangeResult<List<E>> returnResult() {
return getResult();
}
}
private static class DefaultBodyContentSpec implements BodyContentSpec {
private final UndecodedExchangeResult result;
private final ResolvableType elementType;
public DefaultTypeBodySpec(UndecodedExchangeResult result, ResolvableType elementType) {
DefaultBodyContentSpec(UndecodedExchangeResult result) {
this.result = result;
this.elementType = elementType;
}
@Override
public SingleValueBodySpec value() {
return new DefaultSingleValueBodySpec(this.result.consumeSingle(this.elementType));
}
@Override
public ListBodySpec list() {
return list(-1);
}
@Override
public ListBodySpec list(int count) {
return new DefaultListBodySpec(this.result.consumeList(this.elementType, count));
}
@Override
public <T> FluxExchangeResult<T> returnResult() {
return this.result.decodeBody(this.elementType);
}
}
private class DefaultSingleValueBodySpec implements SingleValueBodySpec {
private final EntityExchangeResult<?> result;
public DefaultSingleValueBodySpec(EntityExchangeResult<?> result) {
this.result = result;
}
@Override
public <T> EntityExchangeResult<T> isEqualTo(T expected) {
Object actual = this.result.getResponseBody();
this.result.assertWithDiagnostics(() -> assertEquals("Response body", expected, actual));
return returnResult();
}
@SuppressWarnings("unchecked")
@Override
public <T> EntityExchangeResult<T> returnResult() {
return new EntityExchangeResult<>(this.result, (T) this.result.getResponseBody());
}
}
private class DefaultListBodySpec implements ListBodySpec {
private final EntityExchangeResult<List<?>> result;
public DefaultListBodySpec(EntityExchangeResult<List<?>> result) {
this.result = result;
}
@Override
public <T> EntityExchangeResult<List<T>> isEqualTo(List<T> expected) {
List<?> actual = this.result.getResponseBody();
this.result.assertWithDiagnostics(() -> assertEquals("Response body", expected, actual));
return returnResult();
}
@Override
public ListBodySpec hasSize(int size) {
List<?> actual = this.result.getResponseBody();
String message = "Response body does not contain " + size + " elements";
this.result.assertWithDiagnostics(() -> assertEquals(message, size, actual.size()));
return this;
}
@Override
public ListBodySpec contains(Object... elements) {
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) {
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;
}
@Override
@SuppressWarnings("unchecked")
public <T> EntityExchangeResult<List<T>> returnResult() {
return new EntityExchangeResult<>(this.result, (List<T>) this.result.getResponseBody());
}
}
private class DefaultBodySpec implements BodySpec {
private final UndecodedExchangeResult exchangeResult;
public DefaultBodySpec(UndecodedExchangeResult result) {
this.exchangeResult = result;
}
@Override
public EntityExchangeResult<Void> isEmpty() {
return this.exchangeResult.consumeEmpty();
}
@Override
public MapBodySpec map(Class<?> keyType, Class<?> valueType) {
return map(ResolvableType.forClass(keyType), ResolvableType.forClass(valueType));
}
@Override
public MapBodySpec map(ResolvableType keyType, ResolvableType valueType) {
return new DefaultMapBodySpec(this.exchangeResult.consumeMap(keyType, valueType));
}
}
private class DefaultMapBodySpec implements MapBodySpec {
private final EntityExchangeResult<Map<?, ?>> result;
public DefaultMapBodySpec(EntityExchangeResult<Map<?, ?>> result) {
this.result = result;
}
private Map<?, ?> getBody() {
return this.result.getResponseBody();
}
@Override
public <K, V> EntityExchangeResult<Map<K, V>> isEqualTo(Map<K, V> expected) {
String message = "Response body map";
this.result.assertWithDiagnostics(() -> assertEquals(message, expected, getBody()));
return returnResult();
}
@Override
public MapBodySpec hasSize(int size) {
String message = "Response body map size";
this.result.assertWithDiagnostics(() -> assertEquals(message, size, getBody().size()));
return this;
}
@Override
public MapBodySpec contains(Object key, Object value) {
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) {
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) {
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;
}
@Override
@SuppressWarnings("unchecked")
public <K, V> EntityExchangeResult<Map<K, V>> returnResult() {
return new EntityExchangeResult<>(this.result, (Map<K, V>) getBody());
return this.result.decodeToEmpty();
}
}

View File

@ -491,177 +491,110 @@ public interface WebTestClient {
interface ResponseSpec {
/**
* Assertions on the response status.
* Declare expectations on the response status.
*/
StatusAssertions expectStatus();
/**
* Assertions on the headers of the response.
* Declared expectations on the headers of the response.
*/
HeaderAssertions expectHeader();
/**
* Assertions on the body of the response extracted to one or more
* representations of the given type.
* Declare expectations on the response body decoded to {@code <B>}.
* @param bodyType the expected body type
*/
TypeBodySpec expectBody(Class<?> elementType);
<B> BodySpec<B, ?> expectBody(Class<B> bodyType);
/**
* Variant of {@link #expectBody(Class)} for use with generic types.
* Variant of {@link #expectBody(Class)} for a body type with generics.
*/
TypeBodySpec expectBody(ResolvableType elementType);
<B> BodySpec<B, ?> expectBody(ResolvableType bodyType);
/**
* Other assertions on the response body -- isEmpty, map, etc.
* Declare expectations on the response body decoded to {@code List<E>}.
* @param elementType the expected List element type
*/
BodySpec expectBody();
<E> ListBodySpec<E> expectBodyList(Class<E> elementType);
/**
* Variant of {@link #expectBodyList(Class)} for element types with generics.
*/
<E> ListBodySpec<E> expectBodyList(ResolvableType elementType);
/**
* Declare expectations on the response body content.
*/
BodyContentSpec expectBody();
/**
* Return the exchange result with the body decoded to {@code Flux<T>}.
* Use this option for infinite streams and consume the stream with
* the {@code StepVerifier} from the Reactor Add-Ons.
*
* @see <a href="https://github.com/reactor/reactor-addons">
* https://github.com/reactor/reactor-addons</a>
*/
<T> FluxExchangeResult<T> returnResult(Class<T> elementType);
/**
* Variant of {@link #returnResult(Class)} for element types with generics.
*/
<T> FluxExchangeResult<T> returnResult(ResolvableType elementType);
}
/**
* Specification for extracting entities from the response body.
* Specification for asserting a response body decoded to a single Object.
*/
interface TypeBodySpec {
/**
* Extract a single representations from the response.
*/
SingleValueBodySpec value();
/**
* Extract a list of representations from the response.
*/
ListBodySpec list();
/**
* Extract a list of representations consuming the first N elements.
*/
ListBodySpec list(int elementCount);
/**
* 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();
}
/**
* Specification to assert a single value extracted from the response body.
*/
interface SingleValueBodySpec {
interface BodySpec<B, S extends BodySpec<B, S>> {
/**
* Assert the extracted body is equal to the given value.
*/
<T> EntityExchangeResult<T> isEqualTo(T expected);
<T extends S> T isEqualTo(B expected);
/**
* Return request and response details for the exchange including the
* extracted response body.
* Return the exchange result with the decoded body.
*/
<T> EntityExchangeResult<T> returnResult();
EntityExchangeResult<B> returnResult();
}
/**
* Specification to assert a list of values extracted from the response.
* Specification for asserting a response body decoded to a List.
*/
interface ListBodySpec {
/**
* Assert the extracted body is equal to the given list.
*/
<T> EntityExchangeResult<List<T>> isEqualTo(List<T> expected);
interface ListBodySpec<E> extends BodySpec<List<E>, ListBodySpec<E>> {
/**
* Assert the extracted list of values is of the given size.
* @param size the expected size
*/
ListBodySpec hasSize(int size);
ListBodySpec<E> hasSize(int size);
/**
* Assert the extracted list of values contains the given elements.
* @param elements the elements to check
*/
ListBodySpec contains(Object... elements);
@SuppressWarnings("unchecked")
ListBodySpec<E> contains(E... elements);
/**
* Assert the extracted list of values doesn't contain the given elements.
* @param elements the elements to check
*/
ListBodySpec doesNotContain(Object... elements);
@SuppressWarnings("unchecked")
ListBodySpec<E> doesNotContain(E... elements);
/**
* Return request and response details for the exchange including the
* extracted response body.
*/
<T> EntityExchangeResult<List<T>> returnResult();
}
/**
* Specification to apply additional assertions on the response body.
*/
interface BodySpec {
interface BodyContentSpec {
/**
* Consume the body and verify it is empty.
* @return request and response details from the exchange
* @return the exchange result
*/
EntityExchangeResult<Void> isEmpty();
/**
* Extract the response body as a Map with the given key and value type.
*/
MapBodySpec map(Class<?> keyType, Class<?> valueType);
/**
* Variant of {@link #map(Class, Class)} for use with generic types.
*/
MapBodySpec map(ResolvableType keyType, ResolvableType valueType);
}
/**
* Specification to assert response the body extracted as a map.
*/
interface MapBodySpec {
/**
* Assert the extracted map is equal to the given list of elements.
*/
<K, V> EntityExchangeResult<Map<K, V>> isEqualTo(Map<K, V> expected);
/**
* Assert the extracted map has the given size.
* @param size the expected size
*/
MapBodySpec hasSize(int size);
/**
* Assert the extracted map contains the given key value pair.
* @param key the key to check
* @param value the value to check
*/
MapBodySpec contains(Object key, Object value);
/**
* Assert the extracted map contains the given keys.
* @param keys the keys to check
*/
MapBodySpec containsKeys(Object... keys);
/**
* Assert the extracted map contains the given values.
* @param values the keys to check
*/
MapBodySpec containsValues(Object... values);
/**
* Return request and response details for the exchange including the
* extracted response body.
*/
<K, V> EntityExchangeResult<Map<K, V>> returnResult();
}
}

View File

@ -38,7 +38,7 @@ public class DefaultControllerSpecTests {
.get().uri("/")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).value().isEqualTo("Success");
.expectBody(String.class).isEqualTo("Success");
}
@Test
@ -49,7 +49,7 @@ public class DefaultControllerSpecTests {
.get().uri("/exception")
.exchange()
.expectStatus().isBadRequest()
.expectBody(String.class).value().isEqualTo("Handled exception");
.expectBody(String.class).isEqualTo("Handled exception");
}

View File

@ -40,9 +40,10 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static java.time.Duration.*;
import static java.time.Duration.ofMillis;
import static org.hamcrest.CoreMatchers.endsWith;
import static org.junit.Assert.*;
import static org.junit.Assert.assertThat;
import static org.springframework.core.ResolvableType.forClassWithGenerics;
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM;
/**
@ -62,7 +63,7 @@ public class ResponseEntityTests {
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(Person.class).value().isEqualTo(new Person("John"));
.expectBody(Person.class).isEqualTo(new Person("John"));
}
@Test
@ -75,7 +76,7 @@ public class ResponseEntityTests {
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(Person.class).list().isEqualTo(expected);
.expectBodyList(Person.class).isEqualTo(expected);
}
@Test
@ -89,8 +90,7 @@ public class ResponseEntityTests {
this.client.get().uri("/persons?map=true")
.exchange()
.expectStatus().isOk()
.expectBody()
.map(String.class, Person.class).isEqualTo(map);
.expectBody(forClassWithGenerics(Map.class, String.class, Person.class)).isEqualTo(map);
}
@Test
@ -102,8 +102,7 @@ public class ResponseEntityTests {
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(TEXT_EVENT_STREAM)
.expectBody(Person.class)
.returnResult();
.returnResult(Person.class);
StepVerifier.create(result.getResponseBody())
.expectNext(new Person("N0"), new Person("N1"), new Person("N2"))
@ -126,6 +125,7 @@ public class ResponseEntityTests {
@RestController
@RequestMapping("/persons")
@SuppressWarnings("unused")
static class PersonController {
@GetMapping("/{name}")

View File

@ -63,7 +63,7 @@ public class ApplicationContextTests {
this.client.get().uri("/principal")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).value().isEqualTo("Hello Mr. Pablo!");
.expectBody(String.class).isEqualTo("Hello Mr. Pablo!");
}
@Test
@ -72,7 +72,7 @@ public class ApplicationContextTests {
.get().uri("/principal")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).value().isEqualTo("Hello Mr. Giovanni!");
.expectBody(String.class).isEqualTo("Hello Mr. Giovanni!");
}
@Test
@ -83,7 +83,7 @@ public class ApplicationContextTests {
.get().uri("/attributes")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).value().isEqualTo("foo+bar");
.expectBody(String.class).isEqualTo("foo+bar");
}

View File

@ -48,7 +48,7 @@ public class ControllerTests {
this.client.get().uri("/principal")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).value().isEqualTo("Hello Mr. Pablo!");
.expectBody(String.class).isEqualTo("Hello Mr. Pablo!");
}
@Test
@ -57,7 +57,7 @@ public class ControllerTests {
.get().uri("/principal")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).value().isEqualTo("Hello Mr. Giovanni!");
.expectBody(String.class).isEqualTo("Hello Mr. Giovanni!");
}
@Test
@ -68,7 +68,7 @@ public class ControllerTests {
.get().uri("/attributes")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).value().isEqualTo("foo+bar");
.expectBody(String.class).isEqualTo("foo+bar");
}

View File

@ -71,7 +71,7 @@ public class HttpServerTests {
this.client.get().uri("/test")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).value().isEqualTo("It works!");
.expectBody(String.class).isEqualTo("It works!");
}
}

View File

@ -52,7 +52,7 @@ public class RouterFunctionTests {
this.testClient.get().uri("/test")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).value().isEqualTo("It works!");
.expectBody(String.class).isEqualTo("It works!");
}
}