提交 1e8c7e55 编写于 作者: R Rossen Stoyanchev

WebTestClient assert response body with Consumer<B>

Issue: SPR-15421
上级 0e84f246
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -57,6 +57,13 @@ public class ResourceDecoder extends AbstractDecoder<Resource> { ...@@ -57,6 +57,13 @@ public class ResourceDecoder extends AbstractDecoder<Resource> {
public Flux<Resource> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType, public Flux<Resource> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
MimeType mimeType, Map<String, Object> hints) { MimeType mimeType, Map<String, Object> hints) {
return Flux.from(decodeToMono(inputStream, elementType, mimeType, hints));
}
@Override
public Mono<Resource> decodeToMono(Publisher<DataBuffer> inputStream, ResolvableType elementType,
MimeType mimeType, Map<String, Object> hints) {
Class<?> clazz = elementType.getRawClass(); Class<?> clazz = elementType.getRawClass();
Mono<byte[]> byteArray = Flux.from(inputStream). Mono<byte[]> byteArray = Flux.from(inputStream).
...@@ -70,13 +77,13 @@ public class ResourceDecoder extends AbstractDecoder<Resource> { ...@@ -70,13 +77,13 @@ public class ResourceDecoder extends AbstractDecoder<Resource> {
if (InputStreamResource.class.equals(clazz)) { if (InputStreamResource.class.equals(clazz)) {
return Flux.from(byteArray.map(ByteArrayInputStream::new).map(InputStreamResource::new)); return Mono.from(byteArray.map(ByteArrayInputStream::new).map(InputStreamResource::new));
} }
else if (clazz.isAssignableFrom(ByteArrayResource.class)) { else if (clazz.isAssignableFrom(ByteArrayResource.class)) {
return Flux.from(byteArray.map(ByteArrayResource::new)); return Mono.from(byteArray.map(ByteArrayResource::new));
} }
else { else {
return Flux.error(new IllegalStateException("Unsupported resource class: " + clazz)); return Mono.error(new IllegalStateException("Unsupported resource class: " + clazz));
} }
} }
......
...@@ -23,7 +23,9 @@ import java.time.ZonedDateTime; ...@@ -23,7 +23,9 @@ import java.time.ZonedDateTime;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
...@@ -32,13 +34,14 @@ import reactor.core.publisher.Flux; ...@@ -32,13 +34,14 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ClientResponse;
...@@ -47,9 +50,9 @@ import org.springframework.web.reactive.function.client.WebClient; ...@@ -47,9 +50,9 @@ import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriBuilder; import org.springframework.web.util.UriBuilder;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.springframework.test.util.AssertionErrors.assertEquals; import static org.springframework.test.util.AssertionErrors.assertEquals;
import static org.springframework.test.util.AssertionErrors.assertTrue; 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.toFlux;
import static org.springframework.web.reactive.function.BodyExtractors.toMono; import static org.springframework.web.reactive.function.BodyExtractors.toMono;
...@@ -292,6 +295,7 @@ class DefaultWebTestClient implements WebTestClient { ...@@ -292,6 +295,7 @@ class DefaultWebTestClient implements WebTestClient {
ExchangeResult exchangeResult = wiretapConnector.claimRequest(this.requestId); ExchangeResult exchangeResult = wiretapConnector.claimRequest(this.requestId);
return new DefaultResponseSpec(exchangeResult, clientResponse, getTimeout()); return new DefaultResponseSpec(exchangeResult, clientResponse, getTimeout());
} }
} }
...@@ -326,11 +330,12 @@ class DefaultWebTestClient implements WebTestClient { ...@@ -326,11 +330,12 @@ class DefaultWebTestClient implements WebTestClient {
return new FluxExchangeResult<>(this, body, this.timeout); return new FluxExchangeResult<>(this, body, this.timeout);
} }
public EntityExchangeResult<Void> decodeToEmpty() { public EntityExchangeResult<byte[]> decodeToByteArray() {
DataBuffer buffer = this.response.body(toDataBuffers()).blockFirst(this.timeout); ByteArrayResource resource = this.response.body(toMono(ByteArrayResource.class)).block(this.timeout);
assertWithDiagnostics(() -> assertTrue("Expected empty body", buffer == null)); byte[] body = (resource != null ? resource.getByteArray() : null);
return new EntityExchangeResult<>(this, null); return new EntityExchangeResult<>(this, body);
} }
} }
...@@ -375,7 +380,7 @@ class DefaultWebTestClient implements WebTestClient { ...@@ -375,7 +380,7 @@ class DefaultWebTestClient implements WebTestClient {
@Override @Override
public BodyContentSpec expectBody() { public BodyContentSpec expectBody() {
return new DefaultBodyContentSpec(this.result); return new DefaultBodyContentSpec(this.result.decodeToByteArray());
} }
@Override @Override
...@@ -387,6 +392,7 @@ class DefaultWebTestClient implements WebTestClient { ...@@ -387,6 +392,7 @@ class DefaultWebTestClient implements WebTestClient {
public <T> FluxExchangeResult<T> returnResult(ResolvableType elementType) { public <T> FluxExchangeResult<T> returnResult(ResolvableType elementType) {
return this.result.decodeToFlux(elementType); return this.result.decodeToFlux(elementType);
} }
} }
...@@ -406,11 +412,18 @@ class DefaultWebTestClient implements WebTestClient { ...@@ -406,11 +412,18 @@ class DefaultWebTestClient implements WebTestClient {
@Override @Override
public <T extends S> T isEqualTo(B expected) { public <T extends S> T isEqualTo(B expected) {
Object actual = this.result.getResponseBody(); B actual = this.result.getResponseBody();
this.result.assertWithDiagnostics(() -> assertEquals("Response body", expected, actual)); this.result.assertWithDiagnostics(() -> assertEquals("Response body", expected, actual));
return self(); return self();
} }
@Override
public <T extends S> T consumeWith(Consumer<B> consumer) {
B actual = this.result.getResponseBody();
this.result.assertWithDiagnostics(() -> consumer.accept(actual));
return self();
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T extends S> T self() { private <T extends S> T self() {
return (T) this; return (T) this;
...@@ -420,6 +433,7 @@ class DefaultWebTestClient implements WebTestClient { ...@@ -420,6 +433,7 @@ class DefaultWebTestClient implements WebTestClient {
public EntityExchangeResult<B> returnResult() { public EntityExchangeResult<B> returnResult() {
return this.result; return this.result;
} }
} }
...@@ -465,23 +479,55 @@ class DefaultWebTestClient implements WebTestClient { ...@@ -465,23 +479,55 @@ class DefaultWebTestClient implements WebTestClient {
public EntityExchangeResult<List<E>> returnResult() { public EntityExchangeResult<List<E>> returnResult() {
return getResult(); return getResult();
} }
} }
private static class DefaultBodyContentSpec implements BodyContentSpec { private static class DefaultBodyContentSpec implements BodyContentSpec {
private final UndecodedExchangeResult result; private final EntityExchangeResult<byte[]> result;
private final boolean isEmpty;
DefaultBodyContentSpec(UndecodedExchangeResult result) {
DefaultBodyContentSpec(EntityExchangeResult<byte[]> result) {
this.result = result; this.result = result;
this.isEmpty = (result.getResponseBody() == null);
} }
@Override @Override
public EntityExchangeResult<Void> isEmpty() { public EntityExchangeResult<Void> isEmpty() {
return this.result.decodeToEmpty(); this.result.assertWithDiagnostics(() -> assertTrue("Expected empty body", this.isEmpty));
return new EntityExchangeResult<>(this.result, null);
} }
@Override
public BodyContentSpec consumeAsStringWith(Consumer<String> consumer) {
this.result.assertWithDiagnostics(() -> consumer.accept(getBodyAsString()));
return this;
}
private String getBodyAsString() {
if (this.isEmpty) {
return null;
}
MediaType mediaType = this.result.getResponseHeaders().getContentType();
Charset charset = Optional.ofNullable(mediaType).map(MimeType::getCharset).orElse(UTF_8);
return new String(this.result.getResponseBody(), charset);
}
@Override
public BodyContentSpec consumeWith(Consumer<byte[]> consumer) {
this.result.assertWithDiagnostics(() -> consumer.accept(this.result.getResponseBody()));
return this;
}
@Override
public EntityExchangeResult<byte[]> returnResult() {
return this.result;
}
} }
} }
...@@ -486,7 +486,7 @@ public interface WebTestClient { ...@@ -486,7 +486,7 @@ public interface WebTestClient {
} }
/** /**
* Specification for processing the response and applying expectations. * Spec for declaring expectations on the response.
*/ */
interface ResponseSpec { interface ResponseSpec {
...@@ -544,7 +544,7 @@ public interface WebTestClient { ...@@ -544,7 +544,7 @@ public interface WebTestClient {
} }
/** /**
* Specification for asserting a response body decoded to a single Object. * Spec for expectations on the response body decoded to a single Object.
*/ */
interface BodySpec<B, S extends BodySpec<B, S>> { interface BodySpec<B, S extends BodySpec<B, S>> {
...@@ -553,6 +553,11 @@ public interface WebTestClient { ...@@ -553,6 +553,11 @@ public interface WebTestClient {
*/ */
<T extends S> T isEqualTo(B expected); <T extends S> T isEqualTo(B expected);
/**
* Assert the extracted body with the given {@link Consumer}.
*/
<T extends S> T consumeWith(Consumer<B> consumer);
/** /**
* Return the exchange result with the decoded body. * Return the exchange result with the decoded body.
*/ */
...@@ -561,7 +566,7 @@ public interface WebTestClient { ...@@ -561,7 +566,7 @@ public interface WebTestClient {
} }
/** /**
* Specification for asserting a response body decoded to a List. * Spec for expectations on the response body decoded to a List.
*/ */
interface ListBodySpec<E> extends BodySpec<List<E>, ListBodySpec<E>> { interface ListBodySpec<E> extends BodySpec<List<E>, ListBodySpec<E>> {
...@@ -587,6 +592,9 @@ public interface WebTestClient { ...@@ -587,6 +592,9 @@ public interface WebTestClient {
} }
/**
* Spec for expectations on the response body content.
*/
interface BodyContentSpec { interface BodyContentSpec {
/** /**
...@@ -595,6 +603,27 @@ public interface WebTestClient { ...@@ -595,6 +603,27 @@ public interface WebTestClient {
*/ */
EntityExchangeResult<Void> isEmpty(); EntityExchangeResult<Void> isEmpty();
/**
* Assert the response body content converted to a String with the given
* {@link Consumer}. The String is created with the {@link Charset} from
* the "content-type" response header or {@code UTF-8} otherwise.
* @param consumer the consumer for the response body; the input String
* may be {@code null} if there was no response body.
*/
BodyContentSpec consumeAsStringWith(Consumer<String> consumer);
/**
* Assert the response body content with the given {@link Consumer}.
* @param consumer the consumer for the response body; the input
* {@code byte[]} may be {@code null} if there was no response body.
*/
BodyContentSpec consumeWith(Consumer<byte[]> consumer);
/**
* Return the exchange result with body content as {@code byte[]}.
*/
EntityExchangeResult<byte[]> returnResult();
} }
} }
...@@ -42,10 +42,12 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -42,10 +42,12 @@ import org.springframework.web.bind.annotation.RestController;
import static java.time.Duration.ofMillis; import static java.time.Duration.ofMillis;
import static org.hamcrest.CoreMatchers.endsWith; import static org.hamcrest.CoreMatchers.endsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.springframework.core.ResolvableType.forClassWithGenerics; import static org.springframework.core.ResolvableType.forClassWithGenerics;
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM; import static org.springframework.http.MediaType.TEXT_EVENT_STREAM;
/** /**
* Annotated controllers accepting and returning typed Objects. * Annotated controllers accepting and returning typed Objects.
* *
...@@ -66,6 +68,15 @@ public class ResponseEntityTests { ...@@ -66,6 +68,15 @@ public class ResponseEntityTests {
.expectBody(Person.class).isEqualTo(new Person("John")); .expectBody(Person.class).isEqualTo(new Person("John"));
} }
@Test
public void entityWithConsumer() throws Exception {
this.client.get().uri("/persons/John")
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
.expectBody(Person.class).consumeWith(p -> assertEquals(new Person("John"), p));
}
@Test @Test
public void entityList() throws Exception { public void entityList() throws Exception {
......
...@@ -34,6 +34,8 @@ import org.springframework.web.reactive.config.EnableWebFlux; ...@@ -34,6 +34,8 @@ import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilter;
import static org.junit.Assert.assertEquals;
/** /**
* Binding to server infrastructure declared in a Spring ApplicationContext. * Binding to server infrastructure declared in a Spring ApplicationContext.
* *
...@@ -58,14 +60,23 @@ public class ApplicationContextTests { ...@@ -58,14 +60,23 @@ public class ApplicationContextTests {
.build(); .build();
} }
@Test @Test
public void basic() throws Exception { public void bodyContent() throws Exception {
this.client.get().uri("/principal") this.client.get().uri("/principal")
.exchange() .exchange()
.expectStatus().isOk() .expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello Mr. Pablo!"); .expectBody(String.class).isEqualTo("Hello Mr. Pablo!");
} }
@Test
public void bodyContentWithConsumer() throws Exception {
this.client.get().uri("/principal")
.exchange()
.expectStatus().isOk()
.expectBody().consumeAsStringWith(body -> assertEquals("Hello Mr. Pablo!", body));
}
@Test @Test
public void perRequestExchangeMutator() throws Exception { public void perRequestExchangeMutator() throws Exception {
this.client.exchangeMutator(principal("Giovanni")) this.client.exchangeMutator(principal("Giovanni"))
......
...@@ -29,6 +29,8 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -29,6 +29,8 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilter;
import static org.junit.Assert.assertEquals;
/** /**
* Bind to annotated controllers. * Bind to annotated controllers.
* *
...@@ -44,13 +46,21 @@ public class ControllerTests { ...@@ -44,13 +46,21 @@ public class ControllerTests {
@Test @Test
public void basic() throws Exception { public void bodyContent() throws Exception {
this.client.get().uri("/principal") this.client.get().uri("/principal")
.exchange() .exchange()
.expectStatus().isOk() .expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello Mr. Pablo!"); .expectBody(String.class).isEqualTo("Hello Mr. Pablo!");
} }
@Test
public void bodyContentWithConsumer() throws Exception {
this.client.get().uri("/principal")
.exchange()
.expectStatus().isOk()
.expectBody().consumeAsStringWith(body -> assertEquals("Hello Mr. Pablo!", body));
}
@Test @Test
public void perRequestExchangeMutator() throws Exception { public void perRequestExchangeMutator() throws Exception {
this.client.exchangeMutator(principal("Giovanni")) this.client.exchangeMutator(principal("Giovanni"))
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册