Browse Source

WebFlux test support for server endpoints

Issue: SPR-14590
pull/1323/head
Rossen Stoyanchev 9 years ago
parent
commit
4b4201efa1
  1. 3
      build.gradle
  2. 21
      spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java
  3. 199
      spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultControllerSpec.java
  4. 99
      spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebClientSpec.java
  5. 295
      spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java
  6. 55
      spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java
  7. 135
      spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeActions.java
  8. 119
      spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeInfo.java
  9. 125
      spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java
  10. 106
      spring-test/src/main/java/org/springframework/test/web/reactive/server/LoggingExchangeConsumer.java
  11. 107
      spring-test/src/main/java/org/springframework/test/web/reactive/server/MultiValueMapEntryAssertions.java
  12. 54
      spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseContentAssertions.java
  13. 105
      spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseEntityAssertions.java
  14. 99
      spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseEntityCollectionAssertions.java
  15. 80
      spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseHeadersAssertions.java
  16. 594
      spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseStatusAssertions.java
  17. 79
      spring-test/src/main/java/org/springframework/test/web/reactive/server/StringMultiValueMapEntryAssertions.java
  18. 477
      spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java
  19. 102
      spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java
  20. 5
      spring-test/src/main/java/org/springframework/test/web/reactive/server/package-info.java
  21. 66
      spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java
  22. 63
      spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/HeaderTests.java
  23. 159
      spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java
  24. 79
      spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ApplicationContextTests.java
  25. 59
      spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ControllerTests.java
  26. 75
      spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/HttpServerTests.java
  27. 56
      spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/RouterFunctionTests.java

3
build.gradle

@ -1066,6 +1066,7 @@ project("spring-test") { @@ -1066,6 +1066,7 @@ project("spring-test") {
optional(project(":spring-orm"))
optional(project(":spring-web"))
optional(project(":spring-webmvc"))
optional(project(":spring-webflux"))
optional(project(":spring-websocket"))
optional("junit:junit:${junitVersion}")
optional("org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}")
@ -1094,6 +1095,7 @@ project("spring-test") { @@ -1094,6 +1095,7 @@ project("spring-test") {
optional("com.jayway.jsonpath:json-path:2.2.0")
optional("org.reactivestreams:reactive-streams")
optional("io.projectreactor:reactor-core")
optional("io.projectreactor.addons:reactor-test")
testCompile(project(":spring-context-support"))
testCompile(project(":spring-oxm"))
testCompile("javax.mail:javax.mail-api:${javamailVersion}")
@ -1118,6 +1120,7 @@ project("spring-test") { @@ -1118,6 +1120,7 @@ project("spring-test") {
testCompile("org.apache.httpcomponents:httpclient:${httpclientVersion}")
testCompile("javax.cache:cache-api:${cacheApiVersion}")
testCompile('de.bechte.junit:junit-hierarchicalcontextrunner:4.12.1')
testCompile('io.projectreactor.ipc:reactor-netty')
testRuntime("org.junit.jupiter:junit-jupiter-engine:${junitJupiterVersion}")
testRuntime("org.apache.logging.log4j:log4j-jul:${log4jVersion}") // Java Util Logging for JUnit 5
testRuntime("org.ehcache:ehcache:${ehcache3Version}")

21
spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java

@ -67,10 +67,10 @@ public abstract class AssertionErrors { @@ -67,10 +67,10 @@ public abstract class AssertionErrors {
}
/**
* Assert two objects are equal raise an {@link AssertionError} if not.
* Assert two objects are equal and raise an {@link AssertionError} if not.
* <p>For example:
* <pre class="code">
* assertEquals("Response header [" + name + "]", actual, expected);
* assertEquals("Response header [" + name + "]", expected, actual);
* </pre>
* @param message describes the value being checked
* @param expected the expected value
@ -82,4 +82,21 @@ public abstract class AssertionErrors { @@ -82,4 +82,21 @@ public abstract class AssertionErrors {
}
}
/**
* Assert two objects are not equal and raise an {@link AssertionError} otherwise.
* <p>For example:
* <pre class="code">
* assertNotEquals("Response header [" + name + "]", expected, actual);
* </pre>
* @param message describes the value being checked
* @param expected the expected value
* @param actual the actual value
*/
public static void assertNotEquals(String message, Object expected, Object actual) {
if (ObjectUtils.nullSafeEquals(expected, actual)) {
throw new AssertionError(message + " was not expected to be:" +
"<" + ObjectUtils.nullSafeToString(actual) + ">");
}
}
}

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

@ -0,0 +1,199 @@ @@ -0,0 +1,199 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration;
import org.springframework.web.reactive.config.PathMatchConfigurer;
import org.springframework.web.reactive.config.ViewResolverRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
/**
* Default implementation of {@link WebTestClient.ControllerSpec}.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
class DefaultControllerSpec implements WebTestClient.ControllerSpec {
private final List<Object> controllers;
private final TestWebFluxConfigurer configurer = new TestWebFluxConfigurer();
public DefaultControllerSpec(Object... controllers) {
Assert.isTrue(!ObjectUtils.isEmpty(controllers), "At least one controller is required");
this.controllers = Arrays.asList(controllers);
}
@Override
public DefaultControllerSpec contentTypeResolver(Consumer<RequestedContentTypeResolverBuilder> consumer) {
this.configurer.contentTypeResolverConsumer = consumer;
return this;
}
@Override
public DefaultControllerSpec corsMappings(Consumer<CorsRegistry> consumer) {
this.configurer.corsRegistryConsumer = consumer;
return this;
}
@Override
public DefaultControllerSpec pathMatching(Consumer<PathMatchConfigurer> consumer) {
this.configurer.pathMatchConsumer = consumer;
return this;
}
@Override
public DefaultControllerSpec messageReaders(Consumer<List<HttpMessageReader<?>>> consumer) {
this.configurer.readersConsumer = consumer;
return this;
}
@Override
public DefaultControllerSpec messageWriters(Consumer<List<HttpMessageWriter<?>>> consumer) {
this.configurer.writersConsumer = consumer;
return this;
}
@Override
public DefaultControllerSpec formatters(Consumer<FormatterRegistry> consumer) {
this.configurer.formattersConsumer = consumer;
return this;
}
@Override
public DefaultControllerSpec validator(Validator validator) {
this.configurer.validator = validator;
return this;
}
@Override
public DefaultControllerSpec viewResolvers(Consumer<ViewResolverRegistry> consumer) {
this.configurer.viewResolversConsumer = consumer;
return this;
}
@Override
public WebTestClient.WebClientSpec webClientSpec() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
this.controllers.forEach(controller -> registerBean(context, controller));
context.register(DelegatingWebFluxConfiguration.class);
context.registerBean(WebFluxConfigurer.class, () -> this.configurer);
context.refresh();
return WebTestClient.bindToApplicationContext(context);
}
@SuppressWarnings("unchecked")
private <T> void registerBean(AnnotationConfigApplicationContext context, T bean) {
context.registerBean((Class<T>) bean.getClass(), () -> bean);
}
@Override
public WebTestClient build() {
return webClientSpec().build();
}
private class TestWebFluxConfigurer implements WebFluxConfigurer {
private Consumer<RequestedContentTypeResolverBuilder> contentTypeResolverConsumer;
private Consumer<CorsRegistry> corsRegistryConsumer;
private Consumer<PathMatchConfigurer> pathMatchConsumer;
private Consumer<List<HttpMessageReader<?>>> readersConsumer;
private Consumer<List<HttpMessageWriter<?>>> writersConsumer;
private Consumer<FormatterRegistry> formattersConsumer;
private Validator validator;
private Consumer<ViewResolverRegistry> viewResolversConsumer;
@Override
public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
if (this.contentTypeResolverConsumer != null) {
this.contentTypeResolverConsumer.accept(builder);
}
}
@Override
public void addCorsMappings(CorsRegistry registry) {
if (this.corsRegistryConsumer != null) {
this.corsRegistryConsumer.accept(registry);
}
}
@Override
public void configurePathMatching(PathMatchConfigurer configurer) {
if (this.pathMatchConsumer != null) {
this.pathMatchConsumer.accept(configurer);
}
}
@Override
public void extendMessageReaders(List<HttpMessageReader<?>> readers) {
if (this.readersConsumer != null) {
this.readersConsumer.accept(readers);
}
}
@Override
public void extendMessageWriters(List<HttpMessageWriter<?>> writers) {
if (this.writersConsumer != null) {
this.writersConsumer.accept(writers);
}
}
@Override
public void addFormatters(FormatterRegistry registry) {
if (this.formattersConsumer != null) {
this.formattersConsumer.accept(registry);
}
}
@Override
public Optional<Validator> getValidator() {
return Optional.ofNullable(this.validator);
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
if (this.viewResolversConsumer != null) {
this.viewResolversConsumer.accept(registry);
}
}
}
}

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

@ -0,0 +1,99 @@ @@ -0,0 +1,99 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import java.util.Map;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriBuilderFactory;
/**
* Default implementation of {@link WebTestClient.WebClientSpec}.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
class DefaultWebClientSpec implements WebTestClient.WebClientSpec {
private final WebClient.Builder builder = WebClient.builder();
private final ClientHttpConnector connector;
public DefaultWebClientSpec() {
this(new ReactorClientHttpConnector());
}
public DefaultWebClientSpec(HttpHandler httpHandler) {
this(new HttpHandlerConnector(httpHandler));
}
public DefaultWebClientSpec(ClientHttpConnector connector) {
this.connector = connector;
}
@Override
public WebTestClient.WebClientSpec baseUrl(String baseUrl) {
this.builder.baseUrl(baseUrl);
return this;
}
@Override
public WebTestClient.WebClientSpec defaultUriVariables(Map<String, ?> defaultUriVariables) {
this.builder.defaultUriVariables(defaultUriVariables);
return this;
}
@Override
public WebTestClient.WebClientSpec uriBuilderFactory(UriBuilderFactory uriBuilderFactory) {
this.builder.uriBuilderFactory(uriBuilderFactory);
return this;
}
@Override
public WebTestClient.WebClientSpec defaultHeader(String headerName, String... headerValues) {
this.builder.defaultHeader(headerName, headerValues);
return this;
}
@Override
public WebTestClient.WebClientSpec defaultCookie(String cookieName, String... cookieValues) {
this.builder.defaultCookie(cookieName, cookieValues);
return this;
}
@Override
public WebTestClient.WebClientSpec exchangeStrategies(ExchangeStrategies strategies) {
this.builder.exchangeStrategies(strategies);
return this;
}
@Override
public WebTestClient.Builder builder() {
return new DefaultWebTestClientBuilder(this.builder, this.connector);
}
@Override
public WebTestClient build() {
return builder().build();
}
}

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

@ -0,0 +1,295 @@ @@ -0,0 +1,295 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import java.net.URI;
import java.nio.charset.Charset;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriBuilder;
/**
* Default implementation of {@link WebTestClient}.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
class DefaultWebTestClient implements WebTestClient {
private final WebClient webClient;
private final Duration responseTimeout;
private final WiretapConnectorListener connectorListener;
DefaultWebTestClient(WebClient.Builder webClientBuilder, ClientHttpConnector connector, Duration timeout) {
Assert.notNull(webClientBuilder, "WebClient.Builder is required");
WiretapConnector wiretapConnector = new WiretapConnector(connector);
webClientBuilder.clientConnector(wiretapConnector);
this.connectorListener = new WiretapConnectorListener();
wiretapConnector.addListener(this.connectorListener);
this.webClient = webClientBuilder.build();
this.responseTimeout = (timeout != null ? timeout : Duration.ofSeconds(5));
}
private DefaultWebTestClient(DefaultWebTestClient webTestClient, ExchangeFilterFunction filter) {
this.webClient = webTestClient.webClient.filter(filter);
this.connectorListener = webTestClient.connectorListener;
this.responseTimeout = webTestClient.responseTimeout;
}
private Duration getTimeout() {
return this.responseTimeout;
}
@Override
public UriSpec get() {
return toUriSpec(WebClient::get);
}
@Override
public UriSpec head() {
return toUriSpec(WebClient::head);
}
@Override
public UriSpec post() {
return toUriSpec(WebClient::post);
}
@Override
public UriSpec put() {
return toUriSpec(WebClient::put);
}
@Override
public UriSpec patch() {
return toUriSpec(WebClient::patch);
}
@Override
public UriSpec delete() {
return toUriSpec(WebClient::delete);
}
@Override
public UriSpec options() {
return toUriSpec(WebClient::options);
}
private UriSpec toUriSpec(Function<WebClient, WebClient.UriSpec> function) {
return new DefaultUriSpec(function.apply(this.webClient));
}
@Override
public WebTestClient filter(ExchangeFilterFunction filter) {
return new DefaultWebTestClient(this, filter);
}
private class DefaultUriSpec implements UriSpec {
private final WebClient.UriSpec uriSpec;
DefaultUriSpec(WebClient.UriSpec spec) {
this.uriSpec = spec;
}
@Override
public HeaderSpec uri(URI uri) {
return new DefaultHeaderSpec(this.uriSpec.uri(uri));
}
@Override
public HeaderSpec uri(String uriTemplate, Object... uriVariables) {
return new DefaultHeaderSpec(this.uriSpec.uri(uriTemplate, uriVariables));
}
@Override
public HeaderSpec uri(String uriTemplate, Map<String, ?> uriVariables) {
return new DefaultHeaderSpec(this.uriSpec.uri(uriTemplate, uriVariables));
}
@Override
public HeaderSpec uri(Function<UriBuilder, URI> uriBuilder) {
return new DefaultHeaderSpec(this.uriSpec.uri(uriBuilder));
}
}
private class DefaultHeaderSpec implements WebTestClient.HeaderSpec {
private final WebClient.HeaderSpec headerSpec;
private final String requestId;
DefaultHeaderSpec(WebClient.HeaderSpec spec) {
this.headerSpec = spec;
this.requestId = connectorListener.registerRequestId(spec);
}
@Override
public DefaultHeaderSpec header(String headerName, String... headerValues) {
this.headerSpec.header(headerName, headerValues);
return this;
}
@Override
public DefaultHeaderSpec headers(HttpHeaders headers) {
this.headerSpec.headers(headers);
return this;
}
@Override
public DefaultHeaderSpec accept(MediaType... acceptableMediaTypes) {
this.headerSpec.accept(acceptableMediaTypes);
return this;
}
@Override
public DefaultHeaderSpec acceptCharset(Charset... acceptableCharsets) {
this.headerSpec.acceptCharset(acceptableCharsets);
return this;
}
@Override
public DefaultHeaderSpec contentType(MediaType contentType) {
this.headerSpec.contentType(contentType);
return this;
}
@Override
public DefaultHeaderSpec contentLength(long contentLength) {
this.headerSpec.contentLength(contentLength);
return this;
}
@Override
public DefaultHeaderSpec cookie(String name, String value) {
this.headerSpec.cookie(name, value);
return this;
}
@Override
public DefaultHeaderSpec cookies(MultiValueMap<String, String> cookies) {
this.headerSpec.cookies(cookies);
return this;
}
@Override
public DefaultHeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince) {
this.headerSpec.ifModifiedSince(ifModifiedSince);
return this;
}
@Override
public DefaultHeaderSpec ifNoneMatch(String... ifNoneMatches) {
this.headerSpec.ifNoneMatch(ifNoneMatches);
return this;
}
@Override
public ExchangeActions exchange() {
return getExchangeActions(this.headerSpec.exchange());
}
@Override
public <T> ExchangeActions exchange(BodyInserter<T, ? super ClientHttpRequest> inserter) {
return getExchangeActions(this.headerSpec.exchange(inserter));
}
@Override
public <T, S extends Publisher<T>> ExchangeActions exchange(S publisher, Class<T> elementClass) {
return getExchangeActions(this.headerSpec.exchange(publisher, elementClass));
}
private ExchangeActions getExchangeActions(Mono<ClientResponse> responseMono) {
ClientResponse response = responseMono.block(getTimeout());
ExchangeInfo info = getExchangeInfo(response);
return new ExchangeActions(info);
}
private ExchangeInfo getExchangeInfo(ClientResponse clientResponse) {
WiretapConnector.Info wiretapInfo = connectorListener.retrieveRequest(this.requestId);
ClientHttpRequest request = wiretapInfo.getRequest();
return new ExchangeInfo(request.getMethod(), request.getURI(), request.getHeaders(),
clientResponse, getTimeout());
}
}
private static class WiretapConnectorListener implements Consumer<WiretapConnector.Info> {
private static final String REQUEST_ID_HEADER_NAME = "request-id";
private final AtomicLong index = new AtomicLong();
private final Map<String, WiretapConnector.Info> exchanges = new ConcurrentHashMap<>();
public String registerRequestId(WebClient.HeaderSpec headerSpec) {
String requestId = String.valueOf(this.index.incrementAndGet());
headerSpec.header(REQUEST_ID_HEADER_NAME, requestId);
return requestId;
}
@Override
public void accept(WiretapConnector.Info info) {
Optional.ofNullable(info.getRequest().getHeaders().getFirst(REQUEST_ID_HEADER_NAME))
.ifPresent(id -> this.exchanges.put(id, info));
}
public WiretapConnector.Info retrieveRequest(String requestId) {
WiretapConnector.Info info = this.exchanges.remove(requestId);
Assert.notNull(info, "No match for \"request-id\"=" + requestId);
return info;
}
}
}

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

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import java.time.Duration;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
/**
* Default implementation of {@link WebTestClient.Builder}.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
class DefaultWebTestClientBuilder implements WebTestClient.Builder {
private final WebClient.Builder webClientBuilder;
private final ClientHttpConnector connector;
private Duration responseTimeout;
public DefaultWebTestClientBuilder(WebClient.Builder builder, ClientHttpConnector connector) {
this.webClientBuilder = builder;
this.connector = connector;
}
@Override
public WebTestClient.Builder responseTimeout(Duration timeout) {
this.responseTimeout = timeout;
return this;
}
@Override
public WebTestClient build() {
return new DefaultWebTestClient(this.webClientBuilder, this.connector, this.responseTimeout);
}
}

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

@ -0,0 +1,135 @@ @@ -0,0 +1,135 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.function.Consumer;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.test.util.AssertionErrors;
import org.springframework.web.reactive.function.client.ClientResponse;
/**
* An API to apply assertion and other actions against a performed exchange.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public final class ExchangeActions {
private final ExchangeInfo exchangeInfo;
public ExchangeActions(ExchangeInfo info) {
this.exchangeInfo = info;
}
private ClientResponse getResponse() {
return this.exchangeInfo.getResponse();
}
private Duration getResponseTimeout() {
return this.exchangeInfo.getResponseTimeout();
}
/**
* Assert the status of the response.
* @return further options for asserting the status of the response
*/
public ResponseStatusAssertions assertStatus() {
return new ResponseStatusAssertions(this, this.exchangeInfo);
}
/**
* Assert specific, commonly used response headers.
* @return further options for asserting headers
*/
public ResponseHeadersAssertions assertHeaders() {
return new ResponseHeadersAssertions(this, getResponse().headers().asHttpHeaders());
}
/**
* Assert options for any response header specified by name.
* @return options for asserting headers
*/
public StringMultiValueMapEntryAssertions assertHeader(String headerName) {
HttpHeaders headers = getResponse().headers().asHttpHeaders();
return new StringMultiValueMapEntryAssertions(this, headerName, headers, "Response header");
}
/**
* Assert the response is empty.
*/
public void assertNoContent() {
Flux<?> body = getResponse().bodyToFlux(ByteBuffer.class);
StepVerifier.create(body).expectComplete().verify(getResponseTimeout());
}
/**
* Assert the content of the response.
* @param entityType the type of entity to decode the response as
* @param <T> the type of entity
* @return further options for asserting response entities
*/
public <T> ResponseContentAssertions<T> assertEntity(Class<T> entityType) {
return new ResponseContentAssertions<T>(this.exchangeInfo, ResolvableType.forClass(entityType));
}
/**
* Variant of {@link #assertEntity(Class)} with a {@link ResolvableType}.
* @param entityType the type of entity to decode the response as
* @return further options for asserting response entities
*/
public <T> ResponseContentAssertions<T> assertEntity(ResolvableType entityType) {
return new ResponseContentAssertions<T>(this.exchangeInfo, entityType);
}
/**
* Log debug information about the exchange.
*/
public LoggingExchangeConsumer log() {
return new LoggingExchangeConsumer(this, this.exchangeInfo);
}
/**
* Apply custom assertions on the performed exchange with the help of
* {@link AssertionErrors} or an assertion library such as AssertJ.
* <p>Consider using statically imported methods to improve readability
* @param consumer consumer that will apply assertions.
*/
public ExchangeActions andAssert(Consumer<ExchangeInfo> consumer) {
consumer.accept(this.exchangeInfo);
return this;
}
/**
* Apply custom actions on the performed exchange.
* <p>Consider using statically imported methods to improve readability
* @param consumer consumer that will apply the custom action
*/
public ExchangeActions andDo(Consumer<ExchangeInfo> consumer) {
consumer.accept(this.exchangeInfo);
return this;
}
}

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

@ -0,0 +1,119 @@ @@ -0,0 +1,119 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import java.net.URI;
import java.time.Duration;
import java.util.stream.Collectors;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.client.ClientResponse;
/**
* Contains information about a performed exchange.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ExchangeInfo {
private final HttpMethod method;
private final URI url;
private final HttpHeaders requestHeaders;
private final ClientResponse response;
private final Duration responseTimeout;
public ExchangeInfo(HttpMethod httpMethod, URI uri, HttpHeaders requestHeaders,
ClientResponse response, Duration responseTimeout) {
this.method = httpMethod;
this.url = uri;
this.requestHeaders = requestHeaders;
this.response = response;
this.responseTimeout = responseTimeout;
}
/**
* Return the HTTP method of the exchange.
*/
public HttpMethod getHttpMethod() {
return this.method;
}
/**
* Return the URI of the exchange.
*/
public URI getUrl() {
return this.url;
}
/**
* Return the request headers of the exchange.
*/
public HttpHeaders getRequestHeaders() {
return this.requestHeaders;
}
/**
* Return the {@link ClientResponse} for the exchange.
*/
public ClientResponse getResponse() {
return this.response;
}
/**
* Return the configured timeout for blocking on response data.
*/
public Duration getResponseTimeout() {
return this.responseTimeout;
}
@Override
public String toString() {
HttpStatus status = getResponse().statusCode();
return "\n\n" +
formatValue("Request", getHttpMethod() + " " + getUrl()) +
formatValue("Status", status + " " + status.getReasonPhrase()) +
formatHeading("Response Headers") +
formatHeaders(getResponse().headers().asHttpHeaders()) +
formatHeading("Request Headers") +
formatHeaders(getRequestHeaders());
}
private String formatHeading(String heading) {
return "\n" + String.format("%s", heading) + "\n";
}
private String formatValue(String label, Object value) {
return String.format("%18s: %s", label, value) + "\n";
}
private String formatHeaders(HttpHeaders headers) {
return headers.entrySet().stream()
.map(entry -> formatValue(entry.getKey(), entry.getValue()))
.collect(Collectors.joining());
}
}

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

@ -0,0 +1,125 @@ @@ -0,0 +1,125 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import java.net.URI;
import java.util.Optional;
import java.util.function.Function;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.client.reactive.MockClientHttpRequest;
import org.springframework.mock.http.client.reactive.MockClientHttpResponse;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
/**
* Connector that handles requests by invoking an {@link HttpHandler} rather
* than making actual requests to a network socket.
*
* <p>Internally the connector uses and adapts<br>
* {@link MockClientHttpRequest} and {@link MockClientHttpResponse} to<br>
* {@link MockServerHttpRequest} and {@link MockServerHttpResponse}.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class HttpHandlerConnector implements ClientHttpConnector {
private static Log logger = LogFactory.getLog(HttpHandlerConnector.class);
private final HttpHandler handler;
/**
* Constructor with the {@link HttpHandler} to handle requests with.
*/
public HttpHandlerConnector(HttpHandler handler) {
Assert.notNull(handler, "HttpHandler is required");
this.handler = handler;
}
@Override
public Mono<ClientHttpResponse> connect(HttpMethod httpMethod, URI uri,
Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
MonoProcessor<ClientHttpResponse> result = MonoProcessor.create();
MockClientHttpRequest mockClientRequest = new MockClientHttpRequest(httpMethod, uri);
MockServerHttpResponse mockServerResponse = new MockServerHttpResponse();
mockClientRequest.setWriteHandler(requestBody -> {
log("Invoking HttpHandler for ", httpMethod, uri);
ServerHttpRequest mockServerRequest = adaptRequest(mockClientRequest, requestBody);
this.handler.handle(mockServerRequest, mockServerResponse).subscribe(aVoid -> {}, result::onError);
return Mono.empty();
});
mockServerResponse.setWriteHandler(responseBody -> {
log("Creating client response for ", httpMethod, uri);
result.onNext(adaptResponse(mockServerResponse, responseBody));
return Mono.empty();
});
log("Writing client request for ", httpMethod, uri);
requestCallback.apply(mockClientRequest).subscribe(aVoid -> {}, result::onError);
return result;
}
private void log(String message, HttpMethod httpMethod, URI uri) {
if (logger.isDebugEnabled()) {
logger.debug(String.format("%s %s \"%s\"", message, httpMethod, uri));
}
}
private ServerHttpRequest adaptRequest(MockClientHttpRequest request, Publisher<DataBuffer> body) {
HttpMethod method = request.getMethod();
URI uri = request.getURI();
HttpHeaders headers = request.getHeaders();
MultiValueMap<String, HttpCookie> cookies = request.getCookies();
return MockServerHttpRequest.method(method, uri).headers(headers).cookies(cookies).body(body);
}
private ClientHttpResponse adaptResponse(MockServerHttpResponse response, Flux<DataBuffer> body) {
HttpStatus status = Optional.ofNullable(response.getStatusCode()).orElse(HttpStatus.OK);
MockClientHttpResponse clientResponse = new MockClientHttpResponse(status);
clientResponse.getHeaders().putAll(response.getHeaders());
clientResponse.getCookies().putAll(response.getCookies());
clientResponse.setBody(body);
return clientResponse;
}
}

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

@ -0,0 +1,106 @@ @@ -0,0 +1,106 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Provides options for logging information about the performed exchange.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class LoggingExchangeConsumer {
private static Log logger = LogFactory.getLog(LoggingExchangeConsumer.class);
private final ExchangeActions exchangeActions;
private final ExchangeInfo exchangeInfo;
public LoggingExchangeConsumer(ExchangeActions exchangeActions, ExchangeInfo exchangeInfo) {
this.exchangeActions = exchangeActions;
this.exchangeInfo = exchangeInfo;
}
/**
* Log with {@link System#out}.
*/
public ExchangeActions toConsole() {
System.out.println(this.exchangeInfo.toString());
return this.exchangeActions;
}
/**
* Log with a given {@link OutputStream}.
*/
public ExchangeActions toOutputStream(OutputStream stream) {
return toWriter(new PrintWriter(stream, true));
}
/**
* Log with a given {@link Writer}.
*/
public ExchangeActions toWriter(Writer writer) {
try {
writer.write(this.exchangeInfo.toString());
}
catch (IOException ex) {
throw new IllegalStateException("Failed to print exchange info", ex);
}
return this.exchangeActions;
}
/**
* Log if TRACE level logging is enabled.
*/
public ExchangeActions ifTraceEnabled() {
return doLog(Log::isTraceEnabled, Log::trace);
}
/**
* Log if DEBUG level logging is enabled.
*/
public ExchangeActions ifDebugEnabled() {
return doLog(Log::isDebugEnabled, Log::debug);
}
/**
* Log if INFO level logging is enabled.
*/
public ExchangeActions ifInfoEnabled() {
return doLog(Log::isInfoEnabled, Log::info);
}
private ExchangeActions doLog(Predicate<Log> logLevelPredicate, BiConsumer<Log, String> logAction) {
if (logLevelPredicate.test(logger)) {
logAction.accept(logger, this.exchangeInfo.toString());
}
return this.exchangeActions;
}
}

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

@ -0,0 +1,107 @@ @@ -0,0 +1,107 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import java.util.Arrays;
import java.util.List;
import org.springframework.util.MultiValueMap;
import static org.springframework.test.util.AssertionErrors.assertEquals;
import static org.springframework.test.util.AssertionErrors.assertTrue;
import static org.springframework.test.util.AssertionErrors.fail;
/**
* Assertions on the values of a {@link MultiValueMap} entry.
*
* @param <V> the type of values in the map.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class MultiValueMapEntryAssertions<V> {
private final ExchangeActions exchangeActions;
private final String name;
private final MultiValueMap<String, V> map;
private final String errorMessagePrefix;
MultiValueMapEntryAssertions(ExchangeActions actions, String name,
MultiValueMap<String, V> map, String errorMessagePrefix) {
this.exchangeActions = actions;
this.name = name;
this.map = map;
this.errorMessagePrefix = errorMessagePrefix + " " + this.name;
}
/**
* The given values are equal to the actual values.
* @param values the values to match
*/
@SuppressWarnings("unchecked")
public ExchangeActions isEqualTo(V... values) {
List<V> actual = this.map.get(this.name);
assertEquals(this.errorMessagePrefix, Arrays.asList(values), actual);
return this.exchangeActions;
}
/**
* The list of actual values contains the given values.
* @param values the values to match
*/
@SuppressWarnings("unchecked")
public ExchangeActions hasValues(V... values) {
List<V> actual = this.map.get(this.name);
List<V> expected = Arrays.asList(values);
String message = getErrorMessagePrefix() + " does not contain " + expected;
assertTrue(message, actual.containsAll(expected));
return this.exchangeActions;
}
// Protected methods for sub-classes
protected ExchangeActions getExchangeActions() {
return this.exchangeActions;
}
protected String getName() {
return this.name;
}
protected MultiValueMap<String, V> getMap() {
return this.map;
}
protected String getErrorMessagePrefix() {
return this.errorMessagePrefix;
}
protected V getValue(int index) {
List<V> actualValues = getMap().get(getName());
if (actualValues == null || index >= actualValues.size()) {
fail(getErrorMessagePrefix() + " does not have values at index[" + index + "]: " + actualValues);
}
return actualValues.get(index);
}
}

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

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import org.springframework.core.ResolvableType;
import static org.springframework.web.reactive.function.BodyExtractors.toFlux;
/**
* Provides options for asserting the content of the response.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ResponseContentAssertions<T> extends ResponseEntityAssertions<T> {
ResponseContentAssertions(ExchangeInfo exchangeInfo, ResolvableType entityType) {
super(exchangeInfo, entityType);
}
/**
* Assert the response as a collection of entities.
*/
public ResponseEntityCollectionAssertions<T> collection() {
return new ResponseEntityCollectionAssertions<>(getExchangeInfo(), getEntityType());
}
/**
* Assert the response using a {@link StepVerifier}.
*/
public StepVerifier.FirstStep<T> stepVerifier() {
Flux<T> flux = getExchangeInfo().getResponse().body(toFlux(getEntityType()));
return StepVerifier.create(flux);
}
}

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

@ -0,0 +1,105 @@ @@ -0,0 +1,105 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import java.util.function.Consumer;
import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.test.util.AssertionErrors;
import static org.springframework.test.util.AssertionErrors.assertEquals;
import static org.springframework.web.reactive.function.BodyExtractors.toMono;
/**
* Provides methods for asserting the response body as a single entity.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ResponseEntityAssertions<T> {
private final ExchangeInfo exchangeInfo;
private final ResolvableType entityType;
private T entity;
ResponseEntityAssertions(ExchangeInfo info, ResolvableType entityType) {
this.exchangeInfo = info;
this.entityType = entityType;
}
protected ExchangeInfo getExchangeInfo() {
return this.exchangeInfo;
}
protected ResolvableType getEntityType() {
return this.entityType;
}
private T resolveEntity() {
if (this.entity == null) {
Mono<T> mono = this.exchangeInfo.getResponse().body(toMono(entityType));
this.entity = mono.block(this.exchangeInfo.getResponseTimeout());
}
return this.entity;
}
/**
* Assert the response entity is equal to the given expected entity.
*/
public ResponseEntityAssertions<T> isEqualTo(T expected) {
assertEquals("Response body", expected, resolveEntity());
return this;
}
/**
* Assert the response entity is not equal to the given expected entity.
*/
public ResponseEntityAssertions<T> isNotEqualTo(T expected) {
assertEquals("Response body", expected, resolveEntity());
return this;
}
/**
* Apply custom assertions on the response entity with the help of
* {@link AssertionErrors} or an assertion library such as AssertJ.
* <p>Consider using statically imported methods for creating the assertion
* consumer to improve readability of tests.
* @param assertionConsumer consumer that will apply assertions.
*/
public ResponseEntityAssertions<T> andAssert(Consumer<T> assertionConsumer) {
assertionConsumer.accept(resolveEntity());
return this;
}
/**
* Apply custom actions on the response entity collection.
* <p>Consider using statically imported methods for creating the assertion
* consumer to improve readability of tests.
* @param consumer consumer that will apply the custom action
*/
public ResponseEntityAssertions<T> andDo(Consumer<T> consumer) {
consumer.accept(resolveEntity());
return this;
}
}

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

@ -0,0 +1,99 @@ @@ -0,0 +1,99 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import java.util.Arrays;
import java.util.Collection;
import java.util.function.Consumer;
import reactor.core.publisher.Flux;
import org.springframework.core.ResolvableType;
import org.springframework.test.util.AssertionErrors;
import static org.springframework.test.util.AssertionErrors.assertEquals;
import static org.springframework.test.util.AssertionErrors.assertTrue;
import static org.springframework.web.reactive.function.BodyExtractors.toFlux;
/**
* Provides methods for asserting the response body as a collection of entities.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ResponseEntityCollectionAssertions<T> {
private final Collection<T> entities;
ResponseEntityCollectionAssertions(ExchangeInfo info, ResolvableType entityType) {
Flux<T> flux = info.getResponse().body(toFlux(entityType));
this.entities = flux.collectList().block(info.getResponseTimeout());
}
/**
* Assert the number of entities.
*/
public ResponseEntityCollectionAssertions<T> hasSize(int size) {
assertEquals("Response entities count", size, this.entities.size());
return this;
}
/**
* Assert that the response contains all of the given entities.
*/
@SuppressWarnings("unchecked")
public ResponseEntityCollectionAssertions<T> contains(T... entities) {
Arrays.stream(entities).forEach(entity ->
assertTrue("Response does not contain " + entity, this.entities.contains(entity)));
return this;
}
/**
* Assert that the response does not contain any of the given entities.
*/
@SuppressWarnings("unchecked")
public ResponseEntityCollectionAssertions<T> doesNotContain(T... entities) {
Arrays.stream(entities).forEach(entity ->
assertTrue("Response should not contain " + entity, !this.entities.contains(entity)));
return this;
}
/**
* Apply custom assertions on the response entity collection with the help of
* {@link AssertionErrors} or an assertion library such as AssertJ.
* <p>Consider using statically imported methods for creating the assertion
* consumer to improve readability of tests.
* @param assertionConsumer consumer that will apply assertions.
*/
public ResponseEntityCollectionAssertions<T> andAssert(Consumer<Collection<T>> assertionConsumer) {
assertionConsumer.accept(this.entities);
return this;
}
/**
* Apply custom actions on the response entity collection.
* <p>Consider using statically imported methods for creating the assertion
* consumer to improve readability of tests.
* @param consumer consumer that will apply the custom action
*/
public ResponseEntityCollectionAssertions<T> andDo(Consumer<Collection<T>> consumer) {
consumer.accept(this.entities);
return this;
}
}

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

@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import org.springframework.http.CacheControl;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import static org.springframework.test.util.AssertionErrors.assertEquals;
/**
* Provides methods for asserting specific, commonly used response headers.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ResponseHeadersAssertions {
private final ExchangeActions exchangeActions;
private final HttpHeaders headers;
ResponseHeadersAssertions(ExchangeActions actions, HttpHeaders headers) {
this.exchangeActions = actions;
this.headers = headers;
}
public ExchangeActions cacheControl(CacheControl cacheControl) {
String actual = this.headers.getCacheControl();
assertEquals("Response header Cache-Control", cacheControl.getHeaderValue(), actual);
return this.exchangeActions;
}
public ExchangeActions contentDisposition(ContentDisposition contentDisposition) {
ContentDisposition actual = this.headers.getContentDisposition();
assertEquals("Response header Content-Disposition", contentDisposition, actual);
return this.exchangeActions;
}
public ExchangeActions contentLength(long contentLength) {
long actual = this.headers.getContentLength();
assertEquals("Response header Content-Length", contentLength, actual);
return this.exchangeActions;
}
public ExchangeActions contentType(MediaType mediaType) {
MediaType actual = this.headers.getContentType();
assertEquals("Response header Content-Type", mediaType, actual);
return this.exchangeActions;
}
public ExchangeActions expires(int expires) {
long actual = this.headers.getExpires();
assertEquals("Response header Expires", expires, actual);
return this.exchangeActions;
}
public ExchangeActions lastModified(int lastModified) {
long actual = this.headers.getLastModified();
assertEquals("Response header Last-Modified", lastModified, actual);
return this.exchangeActions;
}
}

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

@ -0,0 +1,594 @@ @@ -0,0 +1,594 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import org.springframework.http.HttpStatus;
import static org.springframework.test.util.AssertionErrors.assertEquals;
/**
* Provides methods for asserting the response status.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
@SuppressWarnings("unused")
public class ResponseStatusAssertions {
private final ExchangeActions exchangeActions;
private final HttpStatus httpStatus;
ResponseStatusAssertions(ExchangeActions actions, ExchangeInfo info) {
this.exchangeActions = actions;
this.httpStatus = info.getResponse().statusCode();
}
public ExchangeActions is(int status) {
assertEquals("Response status", status, this.httpStatus.value());
return this.exchangeActions;
}
/**
* Assert the response status code is in the 1xx range.
*/
public ExchangeActions is1xxInformational() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.INFORMATIONAL, this.httpStatus.series());
return this.exchangeActions;
}
/**
* Assert the response status code is in the 2xx range.
*/
public ExchangeActions is2xxSuccessful() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.SUCCESSFUL, this.httpStatus.series());
return this.exchangeActions;
}
/**
* Assert the response status code is in the 3xx range.
*/
public ExchangeActions is3xxRedirection() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.REDIRECTION, this.httpStatus.series());
return this.exchangeActions;
}
/**
* Assert the response status code is in the 4xx range.
*/
public ExchangeActions is4xxClientError() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.CLIENT_ERROR, this.httpStatus.series());
return this.exchangeActions;
}
/**
* Assert the response status code is in the 5xx range.
*/
public ExchangeActions is5xxServerError() {
String message = "Range for response status value " + this.httpStatus;
assertEquals(message, HttpStatus.Series.SERVER_ERROR, this.httpStatus.series());
return this.exchangeActions;
}
/**
* Assert the response error message.
*/
public ExchangeActions reason(String reason) {
assertEquals("Response status reason", reason, this.httpStatus.getReasonPhrase());
return this.exchangeActions;
}
/**
* Assert the response status code is {@code HttpStatus.CONTINUE} (100).
*/
public ExchangeActions isContinue() {
return doMatch(HttpStatus.CONTINUE);
}
/**
* Assert the response status code is {@code HttpStatus.SWITCHING_PROTOCOLS} (101).
*/
public ExchangeActions isSwitchingProtocols() {
return doMatch(HttpStatus.SWITCHING_PROTOCOLS);
}
/**
* Assert the response status code is {@code HttpStatus.PROCESSING} (102).
*/
public ExchangeActions isProcessing() {
return doMatch(HttpStatus.PROCESSING);
}
/**
* Assert the response status code is {@code HttpStatus.CHECKPOINT} (103).
*/
public ExchangeActions isCheckpoint() {
return doMatch(HttpStatus.valueOf(103));
}
/**
* Assert the response status code is {@code HttpStatus.OK} (200).
*/
public ExchangeActions isOk() {
return doMatch(HttpStatus.OK);
}
/**
* Assert the response status code is {@code HttpStatus.CREATED} (201).
*/
public ExchangeActions isCreated() {
return doMatch(HttpStatus.CREATED);
}
/**
* Assert the response status code is {@code HttpStatus.ACCEPTED} (202).
*/
public ExchangeActions isAccepted() {
return doMatch(HttpStatus.ACCEPTED);
}
/**
* Assert the response status code is {@code HttpStatus.NON_AUTHORITATIVE_INFORMATION} (203).
*/
public ExchangeActions isNonAuthoritativeInformation() {
return doMatch(HttpStatus.NON_AUTHORITATIVE_INFORMATION);
}
/**
* Assert the response status code is {@code HttpStatus.NO_CONTENT} (204).
*/
public ExchangeActions isNoContent() {
return doMatch(HttpStatus.NO_CONTENT);
}
/**
* Assert the response status code is {@code HttpStatus.RESET_CONTENT} (205).
*/
public ExchangeActions isResetContent() {
return doMatch(HttpStatus.RESET_CONTENT);
}
/**
* Assert the response status code is {@code HttpStatus.PARTIAL_CONTENT} (206).
*/
public ExchangeActions isPartialContent() {
return doMatch(HttpStatus.PARTIAL_CONTENT);
}
/**
* Assert the response status code is {@code HttpStatus.MULTI_STATUS} (207).
*/
public ExchangeActions isMultiStatus() {
return doMatch(HttpStatus.MULTI_STATUS);
}
/**
* Assert the response status code is {@code HttpStatus.ALREADY_REPORTED} (208).
*/
public ExchangeActions isAlreadyReported() {
return doMatch(HttpStatus.ALREADY_REPORTED);
}
/**
* Assert the response status code is {@code HttpStatus.IM_USED} (226).
*/
public ExchangeActions isImUsed() {
return doMatch(HttpStatus.IM_USED);
}
/**
* Assert the response status code is {@code HttpStatus.MULTIPLE_CHOICES} (300).
*/
public ExchangeActions isMultipleChoices() {
return doMatch(HttpStatus.MULTIPLE_CHOICES);
}
/**
* Assert the response status code is {@code HttpStatus.MOVED_PERMANENTLY} (301).
*/
public ExchangeActions isMovedPermanently() {
return doMatch(HttpStatus.MOVED_PERMANENTLY);
}
/**
* Assert the response status code is {@code HttpStatus.FOUND} (302).
*/
public ExchangeActions isFound() {
return doMatch(HttpStatus.FOUND);
}
/**
* Assert the response status code is {@code HttpStatus.MOVED_TEMPORARILY} (302).
* @see #isFound()
* @deprecated in favor of {@link #isFound()}
*/
@Deprecated
public ExchangeActions isMovedTemporarily() {
return doMatch(HttpStatus.MOVED_TEMPORARILY);
}
/**
* Assert the response status code is {@code HttpStatus.SEE_OTHER} (303).
*/
public ExchangeActions isSeeOther() {
return doMatch(HttpStatus.SEE_OTHER);
}
/**
* Assert the response status code is {@code HttpStatus.NOT_MODIFIED} (304).
*/
public ExchangeActions isNotModified() {
return doMatch(HttpStatus.NOT_MODIFIED);
}
/**
* Assert the response status code is {@code HttpStatus.USE_PROXY} (305).
* @deprecated matching the deprecation of {@code HttpStatus.USE_PROXY}
*/
@Deprecated
public ExchangeActions isUseProxy() {
return doMatch(HttpStatus.USE_PROXY);
}
/**
* Assert the response status code is {@code HttpStatus.TEMPORARY_REDIRECT} (307).
*/
public ExchangeActions isTemporaryRedirect() {
return doMatch(HttpStatus.TEMPORARY_REDIRECT);
}
/**
* Assert the response status code is {@code HttpStatus.PERMANENT_REDIRECT} (308).
*/
public ExchangeActions isPermanentRedirect() {
return doMatch(HttpStatus.valueOf(308));
}
/**
* Assert the response status code is {@code HttpStatus.BAD_REQUEST} (400).
*/
public ExchangeActions isBadRequest() {
return doMatch(HttpStatus.BAD_REQUEST);
}
/**
* Assert the response status code is {@code HttpStatus.UNAUTHORIZED} (401).
*/
public ExchangeActions isUnauthorized() {
return doMatch(HttpStatus.UNAUTHORIZED);
}
/**
* Assert the response status code is {@code HttpStatus.PAYMENT_REQUIRED} (402).
*/
public ExchangeActions isPaymentRequired() {
return doMatch(HttpStatus.PAYMENT_REQUIRED);
}
/**
* Assert the response status code is {@code HttpStatus.FORBIDDEN} (403).
*/
public ExchangeActions isForbidden() {
return doMatch(HttpStatus.FORBIDDEN);
}
/**
* Assert the response status code is {@code HttpStatus.NOT_FOUND} (404).
*/
public ExchangeActions isNotFound() {
return doMatch(HttpStatus.NOT_FOUND);
}
/**
* Assert the response status code is {@code HttpStatus.METHOD_NOT_ALLOWED} (405).
*/
public ExchangeActions isMethodNotAllowed() {
return doMatch(HttpStatus.METHOD_NOT_ALLOWED);
}
/**
* Assert the response status code is {@code HttpStatus.NOT_ACCEPTABLE} (406).
*/
public ExchangeActions isNotAcceptable() {
return doMatch(HttpStatus.NOT_ACCEPTABLE);
}
/**
* Assert the response status code is {@code HttpStatus.PROXY_AUTHENTICATION_REQUIRED} (407).
*/
public ExchangeActions isProxyAuthenticationRequired() {
return doMatch(HttpStatus.PROXY_AUTHENTICATION_REQUIRED);
}
/**
* Assert the response status code is {@code HttpStatus.REQUEST_TIMEOUT} (408).
*/
public ExchangeActions isRequestTimeout() {
return doMatch(HttpStatus.REQUEST_TIMEOUT);
}
/**
* Assert the response status code is {@code HttpStatus.CONFLICT} (409).
*/
public ExchangeActions isConflict() {
return doMatch(HttpStatus.CONFLICT);
}
/**
* Assert the response status code is {@code HttpStatus.GONE} (410).
*/
public ExchangeActions isGone() {
return doMatch(HttpStatus.GONE);
}
/**
* Assert the response status code is {@code HttpStatus.LENGTH_REQUIRED} (411).
*/
public ExchangeActions isLengthRequired() {
return doMatch(HttpStatus.LENGTH_REQUIRED);
}
/**
* Assert the response status code is {@code HttpStatus.PRECONDITION_FAILED} (412).
*/
public ExchangeActions isPreconditionFailed() {
return doMatch(HttpStatus.PRECONDITION_FAILED);
}
/**
* Assert the response status code is {@code HttpStatus.PAYLOAD_TOO_LARGE} (413).
* @since 4.1
*/
public ExchangeActions isPayloadTooLarge() {
return doMatch(HttpStatus.PAYLOAD_TOO_LARGE);
}
/**
* Assert the response status code is {@code HttpStatus.REQUEST_ENTITY_TOO_LARGE} (413).
* @deprecated matching the deprecation of {@code HttpStatus.REQUEST_ENTITY_TOO_LARGE}
* @see #isPayloadTooLarge()
*/
@Deprecated
public ExchangeActions isRequestEntityTooLarge() {
return doMatch(HttpStatus.REQUEST_ENTITY_TOO_LARGE);
}
/**
* Assert the response status code is {@code HttpStatus.REQUEST_URI_TOO_LONG} (414).
* @since 4.1
*/
public ExchangeActions isUriTooLong() {
return doMatch(HttpStatus.URI_TOO_LONG);
}
/**
* Assert the response status code is {@code HttpStatus.REQUEST_URI_TOO_LONG} (414).
* @deprecated matching the deprecation of {@code HttpStatus.REQUEST_URI_TOO_LONG}
* @see #isUriTooLong()
*/
@Deprecated
public ExchangeActions isRequestUriTooLong() {
return doMatch(HttpStatus.REQUEST_URI_TOO_LONG);
}
/**
* Assert the response status code is {@code HttpStatus.UNSUPPORTED_MEDIA_TYPE} (415).
*/
public ExchangeActions isUnsupportedMediaType() {
return doMatch(HttpStatus.UNSUPPORTED_MEDIA_TYPE);
}
/**
* Assert the response status code is {@code HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE} (416).
*/
public ExchangeActions isRequestedRangeNotSatisfiable() {
return doMatch(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
}
/**
* Assert the response status code is {@code HttpStatus.EXPECTATION_FAILED} (417).
*/
public ExchangeActions isExpectationFailed() {
return doMatch(HttpStatus.EXPECTATION_FAILED);
}
/**
* Assert the response status code is {@code HttpStatus.I_AM_A_TEAPOT} (418).
*/
public ExchangeActions isIAmATeapot() {
return doMatch(HttpStatus.valueOf(418));
}
/**
* Assert the response status code is {@code HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE} (419).
* @deprecated matching the deprecation of {@code HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE}
*/
@Deprecated
public ExchangeActions isInsufficientSpaceOnResource() {
return doMatch(HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE);
}
/**
* Assert the response status code is {@code HttpStatus.METHOD_FAILURE} (420).
* @deprecated matching the deprecation of {@code HttpStatus.METHOD_FAILURE}
*/
@Deprecated
public ExchangeActions isMethodFailure() {
return doMatch(HttpStatus.METHOD_FAILURE);
}
/**
* Assert the response status code is {@code HttpStatus.DESTINATION_LOCKED} (421).
* @deprecated matching the deprecation of {@code HttpStatus.DESTINATION_LOCKED}
*/
@Deprecated
public ExchangeActions isDestinationLocked() {
return doMatch(HttpStatus.DESTINATION_LOCKED);
}
/**
* Assert the response status code is {@code HttpStatus.UNPROCESSABLE_ENTITY} (422).
*/
public ExchangeActions isUnprocessableEntity() {
return doMatch(HttpStatus.UNPROCESSABLE_ENTITY);
}
/**
* Assert the response status code is {@code HttpStatus.LOCKED} (423).
*/
public ExchangeActions isLocked() {
return doMatch(HttpStatus.LOCKED);
}
/**
* Assert the response status code is {@code HttpStatus.FAILED_DEPENDENCY} (424).
*/
public ExchangeActions isFailedDependency() {
return doMatch(HttpStatus.FAILED_DEPENDENCY);
}
/**
* Assert the response status code is {@code HttpStatus.UPGRADE_REQUIRED} (426).
*/
public ExchangeActions isUpgradeRequired() {
return doMatch(HttpStatus.UPGRADE_REQUIRED);
}
/**
* Assert the response status code is {@code HttpStatus.PRECONDITION_REQUIRED} (428).
*/
public ExchangeActions isPreconditionRequired() {
return doMatch(HttpStatus.valueOf(428));
}
/**
* Assert the response status code is {@code HttpStatus.TOO_MANY_REQUESTS} (429).
*/
public ExchangeActions isTooManyRequests() {
return doMatch(HttpStatus.valueOf(429));
}
/**
* Assert the response status code is {@code HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE} (431).
*/
public ExchangeActions isRequestHeaderFieldsTooLarge() {
return doMatch(HttpStatus.valueOf(431));
}
/**
* Assert the response status code is {@code HttpStatus.UNAVAILABLE_FOR_LEGAL_REASONS} (451).
* @since 4.3
*/
public ExchangeActions isUnavailableForLegalReasons() {
return doMatch(HttpStatus.valueOf(451));
}
/**
* Assert the response status code is {@code HttpStatus.INTERNAL_SERVER_ERROR} (500).
*/
public ExchangeActions isInternalServerError() {
return doMatch(HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* Assert the response status code is {@code HttpStatus.NOT_IMPLEMENTED} (501).
*/
public ExchangeActions isNotImplemented() {
return doMatch(HttpStatus.NOT_IMPLEMENTED);
}
/**
* Assert the response status code is {@code HttpStatus.BAD_GATEWAY} (502).
*/
public ExchangeActions isBadGateway() {
return doMatch(HttpStatus.BAD_GATEWAY);
}
/**
* Assert the response status code is {@code HttpStatus.SERVICE_UNAVAILABLE} (503).
*/
public ExchangeActions isServiceUnavailable() {
return doMatch(HttpStatus.SERVICE_UNAVAILABLE);
}
/**
* Assert the response status code is {@code HttpStatus.GATEWAY_TIMEOUT} (504).
*/
public ExchangeActions isGatewayTimeout() {
return doMatch(HttpStatus.GATEWAY_TIMEOUT);
}
/**
* Assert the response status code is {@code HttpStatus.HTTP_VERSION_NOT_SUPPORTED} (505).
*/
public ExchangeActions isHttpVersionNotSupported() {
return doMatch(HttpStatus.HTTP_VERSION_NOT_SUPPORTED);
}
/**
* Assert the response status code is {@code HttpStatus.VARIANT_ALSO_NEGOTIATES} (506).
*/
public ExchangeActions isVariantAlsoNegotiates() {
return doMatch(HttpStatus.VARIANT_ALSO_NEGOTIATES);
}
/**
* Assert the response status code is {@code HttpStatus.INSUFFICIENT_STORAGE} (507).
*/
public ExchangeActions isInsufficientStorage() {
return doMatch(HttpStatus.INSUFFICIENT_STORAGE);
}
/**
* Assert the response status code is {@code HttpStatus.LOOP_DETECTED} (508).
*/
public ExchangeActions isLoopDetected() {
return doMatch(HttpStatus.LOOP_DETECTED);
}
/**
* Assert the response status code is {@code HttpStatus.BANDWIDTH_LIMIT_EXCEEDED} (509).
*/
public ExchangeActions isBandwidthLimitExceeded() {
return doMatch(HttpStatus.valueOf(509));
}
/**
* Assert the response status code is {@code HttpStatus.NOT_EXTENDED} (510).
*/
public ExchangeActions isNotExtended() {
return doMatch(HttpStatus.NOT_EXTENDED);
}
/**
* Assert the response status code is {@code HttpStatus.NETWORK_AUTHENTICATION_REQUIRED} (511).
*/
public ExchangeActions isNetworkAuthenticationRequired() {
return doMatch(HttpStatus.valueOf(511));
}
private ExchangeActions doMatch(final HttpStatus status) {
assertEquals("Status", status, this.httpStatus);
return this.exchangeActions;
}
}

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

@ -0,0 +1,79 @@ @@ -0,0 +1,79 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import java.util.Arrays;
import java.util.regex.Pattern;
import org.springframework.util.MultiValueMap;
import static org.springframework.test.util.AssertionErrors.assertTrue;
/**
* Extension of {@link MultiValueMapEntryAssertions} for String values.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class StringMultiValueMapEntryAssertions extends MultiValueMapEntryAssertions<String> {
public StringMultiValueMapEntryAssertions(ExchangeActions actions, String name,
MultiValueMap<String, String> map, String errorMessagePrefix) {
super(actions, name, map, errorMessagePrefix);
}
/**
* The specified value {@link String#contains contains} the given sub-strings.
* @param values the values to match
*/
public ExchangeActions valueContains(CharSequence... values) {
String actual = getValue(0);
Arrays.stream(values).forEach(value -> {
String message = getErrorMessagePrefix() + " does not contain " + value;
assertTrue(message, actual.contains(value));
});
return getExchangeActions();
}
/**
* The specified value does not {@link String#contains contain} the given sub-strings.
* @param values the values to match
*/
public ExchangeActions valueDoesNotContain(CharSequence... values) {
String actual = getValue(0);
Arrays.stream(values).forEach(value -> {
String message = getErrorMessagePrefix() + " contains " + value + " but shouldn't";
assertTrue(message, !actual.contains(value));
});
return getExchangeActions();
}
/**
* The specified value matches the given regex pattern value.
* @param pattern the values to be compiled with {@link Pattern}
*/
public ExchangeActions valueMatches(String pattern) {
String actual = getValue(0);
boolean match = Pattern.compile(pattern).matcher(actual).matches();
String message = getErrorMessagePrefix() + " with value " + actual + " does not match " + pattern;
assertTrue(message, match);
return getExchangeActions();
}
}

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

@ -0,0 +1,477 @@ @@ -0,0 +1,477 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import java.net.URI;
import java.nio.charset.Charset;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import org.springframework.context.ApplicationContext;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.PathMatchConfigurer;
import org.springframework.web.reactive.config.ViewResolverRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.server.adapter.HttpWebHandlerAdapter;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import org.springframework.web.util.UriBuilder;
import org.springframework.web.util.UriBuilderFactory;
/**
* Main entry point for testing WebFlux server endpoints with an API similar to
* that of {@link WebClient}, and actually delegating to a {@code WebClient}
* instance, but with a focus on testing.
*
* <p>The {@code WebTestClient} has 3 setup options without a running server:
* <ul>
* <li>{@link #bindToController}
* <li>{@link #bindToApplicationContext}
* <li>{@link #bindToRouterFunction}
* </ul>
* <p>and 1 option for actual requests on a socket:
* <ul>
* <li>{@link #bindToServer()}
* </ul>
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public interface WebTestClient {
/**
* Prepare an HTTP GET request.
* @return a spec for specifying the target URL
*/
UriSpec get();
/**
* Prepare an HTTP HEAD request.
* @return a spec for specifying the target URL
*/
UriSpec head();
/**
* Prepare an HTTP POST request.
* @return a spec for specifying the target URL
*/
UriSpec post();
/**
* Prepare an HTTP PUT request.
* @return a spec for specifying the target URL
*/
UriSpec put();
/**
* Prepare an HTTP PATCH request.
* @return a spec for specifying the target URL
*/
UriSpec patch();
/**
* Prepare an HTTP DELETE request.
* @return a spec for specifying the target URL
*/
UriSpec delete();
/**
* Prepare an HTTP OPTIONS request.
* @return a spec for specifying the target URL
*/
UriSpec options();
/**
* Filter the client with the given {@code ExchangeFilterFunction}.
* @param filterFunction the filter to apply to this client
* @return the filtered client
* @see ExchangeFilterFunction#apply(ExchangeFunction)
*/
WebTestClient filter(ExchangeFilterFunction filterFunction);
// Static, factory methods
/**
* Integration testing without a server, targeting specific annotated,
* WebFlux controllers. The default configuration is the same as for
* {@link org.springframework.web.reactive.config.EnableWebFlux @EnableWebFlux}
* but can also be further customized through the returned spec.
* @param controllers the controllers to test
* @return spec for controller configuration and test client builder
*/
static ControllerSpec bindToController(Object... controllers) {
return new DefaultControllerSpec(controllers);
}
/**
* Integration testing without a server, with WebFlux infrastructure detected
* from an {@link ApplicationContext} such as {@code @EnableWebFlux}
* Java config and annotated controller Spring beans.
* @param applicationContext the context
* @return the {@link WebTestClient} builder
* @see org.springframework.web.reactive.config.EnableWebFlux
*/
static WebClientSpec bindToApplicationContext(ApplicationContext applicationContext) {
HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(applicationContext).build();
return new DefaultWebClientSpec(httpHandler);
}
/**
* Integration testing without a server, targeting WebFlux functional endpoints.
* @param routerFunction the RouterFunction to test
* @return the {@link WebTestClient} builder
*/
static WebClientSpec bindToRouterFunction(RouterFunction<?> routerFunction) {
HttpWebHandlerAdapter httpHandler = RouterFunctions.toHttpHandler(routerFunction);
return new DefaultWebClientSpec(httpHandler);
}
/**
* Complete end-to-end integration tests with actual requests to a running server.
* @return the {@link WebTestClient} builder
*/
static WebClientSpec bindToServer() {
return new DefaultWebClientSpec();
}
/**
* Specification for customizing controller configuration equivalent to, and
* internally delegating to, a {@link WebFluxConfigurer}.
*/
interface ControllerSpec {
/**
* Customize content type resolution.
* @see WebFluxConfigurer#configureContentTypeResolver
*/
ControllerSpec contentTypeResolver(Consumer<RequestedContentTypeResolverBuilder> consumer);
/**
* Configure CORS support.
* @see WebFluxConfigurer#addCorsMappings
*/
ControllerSpec corsMappings(Consumer<CorsRegistry> consumer);
/**
* Configure path matching options.
* @see WebFluxConfigurer#configurePathMatching
*/
ControllerSpec pathMatching(Consumer<PathMatchConfigurer> consumer);
/**
* Modify or extend the list of built-in message readers.
* @see WebFluxConfigurer#configureMessageReaders
*/
ControllerSpec messageReaders(Consumer<List<HttpMessageReader<?>>> readers);
/**
* Modify or extend the list of built-in message writers.
* @see WebFluxConfigurer#configureMessageWriters
*/
ControllerSpec messageWriters(Consumer<List<HttpMessageWriter<?>>> writers);
/**
* Register formatters and converters to use for type conversion.
* @see WebFluxConfigurer#addFormatters
*/
ControllerSpec formatters(Consumer<FormatterRegistry> consumer);
/**
* Configure a global Validator.
* @see WebFluxConfigurer#getValidator()
*/
ControllerSpec validator(Validator validator);
/**
* Configure view resolution.
* @see WebFluxConfigurer#configureViewResolvers
*/
ControllerSpec viewResolvers(Consumer<ViewResolverRegistry> consumer);
/**
* Proceed to configure the {@link WebClient} to test with.
*/
WebClientSpec webClientSpec();
/**
* Shortcut to build the {@link WebTestClient}.
*/
WebTestClient build();
}
/**
* Steps for customizing the {@link WebClient} used to test with
* internally delegating to a {@link WebClient.Builder}.
*/
interface WebClientSpec {
/**
* Configure a base URI as described in
* {@link org.springframework.web.reactive.function.client.WebClient#create(String)
* WebClient.create(String)}.
* @see #defaultUriVariables(Map)
* @see #uriBuilderFactory(UriBuilderFactory)
*/
WebClientSpec baseUrl(String baseUrl);
/**
* Configure default URI variable values that will be used when expanding
* URI templates using a {@link Map}.
* @param defaultUriVariables the default values to use
* @see #baseUrl(String)
* @see #uriBuilderFactory(UriBuilderFactory)
*/
WebClientSpec defaultUriVariables(Map<String, ?> defaultUriVariables);
/**
* Provide a pre-configured {@link UriBuilderFactory} instance. This is
* an alternative to and effectively overrides the following:
* <ul>
* <li>{@link #baseUrl(String)}
* <li>{@link #defaultUriVariables(Map)}.
* </ul>
* @param uriBuilderFactory the URI builder factory to use
* @see #baseUrl(String)
* @see #defaultUriVariables(Map)
*/
WebClientSpec uriBuilderFactory(UriBuilderFactory uriBuilderFactory);
/**
* Add the given header to all requests that haven't added it.
* @param headerName the header name
* @param headerValues the header values
*/
WebClientSpec defaultHeader(String headerName, String... headerValues);
/**
* Add the given header to all requests that haven't added it.
* @param cookieName the cookie name
* @param cookieValues the cookie values
*/
WebClientSpec defaultCookie(String cookieName, String... cookieValues);
/**
* Configure the {@link ExchangeStrategies} to use.
* <p>By default {@link ExchangeStrategies#withDefaults()} is used.
* @param strategies the strategies to use
*/
WebClientSpec exchangeStrategies(ExchangeStrategies strategies);
/**
* Proceed to building the {@link WebTestClient}.
*/
Builder builder();
/**
* Shortcut to build the {@link WebTestClient}.
*/
WebTestClient build();
}
/**
* Build steps to create a {@link WebTestClient}.
*/
interface Builder {
/**
* Max amount of time to wait for responses.
* <p>By default 5 seconds.
* @param timeout the response timeout value
*/
Builder responseTimeout(Duration timeout);
/**
* Build the {@link WebTestClient} instance.
*/
WebTestClient build();
}
/**
* Contract for specifying the URI for a request.
*/
interface UriSpec {
/**
* Specify the URI using an absolute, fully constructed {@link URI}.
*/
HeaderSpec uri(URI uri);
/**
* Specify the URI for the request using a URI template and URI variables.
* If a {@link UriBuilderFactory} was configured for the client (e.g.
* with a base URI) it will be used to expand the URI template.
*/
HeaderSpec uri(String uri, Object... uriVariables);
/**
* Specify the URI for the request using a URI template and URI variables.
* If a {@link UriBuilderFactory} was configured for the client (e.g.
* with a base URI) it will be used to expand the URI template.
*/
HeaderSpec uri(String uri, Map<String, ?> uriVariables);
/**
* Build the URI for the request with a {@link UriBuilder} obtained
* through the {@link UriBuilderFactory} configured for this client.
*/
HeaderSpec uri(Function<UriBuilder, URI> uriFunction);
}
/**
* Contract for specifying request headers leading up to the exchange.
*/
interface HeaderSpec {
/**
* Set the list of acceptable {@linkplain MediaType media types}, as
* specified by the {@code Accept} header.
* @param acceptableMediaTypes the acceptable media types
* @return this builder
*/
HeaderSpec accept(MediaType... acceptableMediaTypes);
/**
* Set the list of acceptable {@linkplain Charset charsets}, as specified
* by the {@code Accept-Charset} header.
* @param acceptableCharsets the acceptable charsets
* @return this builder
*/
HeaderSpec acceptCharset(Charset... acceptableCharsets);
/**
* Set the length of the body in bytes, as specified by the
* {@code Content-Length} header.
* @param contentLength the content length
* @return this builder
* @see HttpHeaders#setContentLength(long)
*/
HeaderSpec contentLength(long contentLength);
/**
* Set the {@linkplain MediaType media type} of the body, as specified
* by the {@code Content-Type} header.
* @param contentType the content type
* @return this builder
* @see HttpHeaders#setContentType(MediaType)
*/
HeaderSpec contentType(MediaType contentType);
/**
* Add a cookie with the given name and value.
* @param name the cookie name
* @param value the cookie value
* @return this builder
*/
HeaderSpec cookie(String name, String value);
/**
* Copy the given cookies into the entity's cookies map.
*
* @param cookies the existing cookies to copy from
* @return this builder
*/
HeaderSpec cookies(MultiValueMap<String, String> cookies);
/**
* Set the value of the {@code If-Modified-Since} header.
* <p>The date should be specified as the number of milliseconds since
* January 1, 1970 GMT.
* @param ifModifiedSince the new value of the header
* @return this builder
*/
HeaderSpec ifModifiedSince(ZonedDateTime ifModifiedSince);
/**
* Set the values of the {@code If-None-Match} header.
* @param ifNoneMatches the new value of the header
* @return this builder
*/
HeaderSpec ifNoneMatch(String... ifNoneMatches);
/**
* Add the given, single header value under the given name.
* @param headerName the header name
* @param headerValues the header value(s)
* @return this builder
*/
HeaderSpec header(String headerName, String... headerValues);
/**
* Copy the given headers into the entity's headers map.
* @param headers the existing headers to copy from
* @return this builder
*/
HeaderSpec headers(HttpHeaders headers);
/**
* Perform the request without a request body.
* @return options for asserting the response with
*/
ExchangeActions exchange();
/**
* Set the body of the request to the given {@code BodyInserter} and
* perform the request.
* @param inserter the {@code BodyInserter} that writes to the request
* @param <T> the type contained in the body
* @return options for asserting the response with
*/
<T> ExchangeActions exchange(BodyInserter<T, ? super ClientHttpRequest> inserter);
/**
* Set the body of the request to the given {@code Publisher} and
* perform the request.
* @param publisher the {@code Publisher} to write to the request
* @param elementClass the class of elements contained in the publisher
* @param <T> the type of the elements contained in the publisher
* @param <S> the type of the {@code Publisher}
* @return options for asserting the response with
*/
<T, S extends Publisher<T>> ExchangeActions exchange(S publisher, Class<T> elementClass);
}
}

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

@ -0,0 +1,102 @@ @@ -0,0 +1,102 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server;
import java.net.URI;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.client.reactive.ClientHttpResponse;
/**
* Decorates a {@link ClientHttpConnector} in order to capture executed requests
* and responses and notify one or more registered listeners. This is helpful
* for access to the actual {@link ClientHttpRequest} sent and the
* {@link ClientHttpResponse} returned by the server.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
class WiretapConnector implements ClientHttpConnector {
private final ClientHttpConnector delegate;
private final List<Consumer<Info>> listeners;
public WiretapConnector(ClientHttpConnector delegate) {
this.delegate = delegate;
this.listeners = new CopyOnWriteArrayList<>();
}
/**
* Register a listener to consume exchanged requests and responses.
*/
public void addListener(Consumer<Info> consumer) {
this.listeners.add(consumer);
}
@Override
public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
AtomicReference<ClientHttpRequest> requestRef = new AtomicReference<>();
return this.delegate
.connect(method, uri, request -> {
requestRef.set(request);
return requestCallback.apply(request);
})
.doOnNext(response -> {
Info info = new Info(requestRef.get(), response);
this.listeners.forEach(consumer -> consumer.accept(info));
});
}
public static class Info {
private final ClientHttpRequest request;
private final ClientHttpResponse response;
public Info(ClientHttpRequest request, ClientHttpResponse response) {
this.request = request;
this.response = response;
}
public ClientHttpRequest getRequest() {
return this.request;
}
public ClientHttpResponse getResponse() {
return this.response;
}
}
}

5
spring-test/src/main/java/org/springframework/test/web/reactive/server/package-info.java

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
/**
* Support for testing Spring WebFlux server endpoints via
* {@link org.springframework.test.web.reactive.server.WebTestClient}.
*/
package org.springframework.test.web.reactive.server;

66
spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server.samples;
import org.junit.Before;
import org.junit.Test;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Tests with error status codes or error conditions.
*
* @author Rossen Stoyanchev
*/
@SuppressWarnings("unused")
public class ErrorTests {
private WebTestClient client;
@Before
public void setUp() throws Exception {
this.client = WebTestClient.bindToController(new TestController()).build();
}
@Test
public void notFound() throws Exception {
this.client.get().uri("/invalid")
.exchange()
.assertStatus().isNotFound();
}
@Test
public void serverException() throws Exception {
this.client.get().uri("/server-error")
.exchange()
.assertStatus().isInternalServerError();
}
@RestController
static class TestController {
@GetMapping("/server-error")
void handleAndThrowException() {
throw new IllegalStateException("server error");
}
}
}

63
spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/HeaderTests.java

@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server.samples;
import org.junit.Before;
import org.junit.Test;
import org.springframework.http.ResponseEntity;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
/**
* Tests with custom headers.
*
* @author Rossen Stoyanchev
*/
@SuppressWarnings("unused")
public class HeaderTests {
private WebTestClient client;
@Before
public void setUp() throws Exception {
this.client = WebTestClient.bindToController(new TestController()).build();
}
@Test
public void customHeader() throws Exception {
this.client.get().uri("/header").header("h1", "ping")
.exchange()
.assertStatus().isOk()
.assertHeader("h1").isEqualTo("ping-pong");
}
@RestController
static class TestController {
@GetMapping("header")
ResponseEntity<Void> handleHeader(@RequestHeader("h1") String myHeader) {
String value = myHeader + "-pong";
return ResponseEntity.ok().header("h1", value).build();
}
}
}

159
spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java

@ -0,0 +1,159 @@ @@ -0,0 +1,159 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server.samples;
import java.net.URI;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.hamcrest.CoreMatchers.endsWith;
import static org.junit.Assert.assertThat;
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM;
/**
* Annotated controllers accepting and returning typed Objects.
*
* @author Rossen Stoyanchev
*/
@SuppressWarnings("unused")
public class ResponseEntityTests {
private WebTestClient client;
@Before
public void setUp() throws Exception {
this.client = WebTestClient.bindToController(new PersonController()).build();
}
@Test
public void entity() throws Exception {
this.client.get().uri("/persons/John")
.exchange()
.assertStatus().isOk()
.assertHeaders().contentType(MediaType.APPLICATION_JSON_UTF8)
.assertEntity(Person.class).isEqualTo(new Person("John"));
}
@Test
public void entityCollection() throws Exception {
this.client.get().uri("/persons")
.exchange()
.assertStatus().isOk()
.assertHeaders().contentType(MediaType.APPLICATION_JSON_UTF8)
.assertEntity(Person.class).collection()
.hasSize(3)
.contains(new Person("Jane"), new Person("Jason"), new Person("John"));
}
@Test
public void entityStream() throws Exception {
this.client.get().uri("/persons").accept(TEXT_EVENT_STREAM)
.exchange()
.assertStatus().isOk()
.assertHeaders().contentType(TEXT_EVENT_STREAM)
.assertEntity(Person.class).stepVerifier()
.expectNext(new Person("N0"), new Person("N1"), new Person("N2"))
.expectNextCount(4)
.consumeNextWith(person -> assertThat(person.getName(), endsWith("7")))
.thenCancel()
.verify();
}
@Test
public void saveEntity() throws Exception {
this.client.post().uri("/persons")
.exchange(Mono.just(new Person("John")), Person.class)
.assertStatus().isCreated()
.assertHeader("location").isEqualTo("/persons/John")
.assertNoContent();
}
@RestController
@RequestMapping("/persons")
static class PersonController {
@GetMapping("/{name}")
Person getPerson(@PathVariable String name) {
return new Person(name);
}
@GetMapping
Flux<Person> getPersons() {
return Flux.just(new Person("Jane"), new Person("Jason"), new Person("John"));
}
@GetMapping(produces = "text/event-stream")
Flux<Person> getPersonStream() {
return Flux.intervalMillis(100).onBackpressureBuffer(10).map(index -> new Person("N" + index));
}
@PostMapping
ResponseEntity<String> savePerson(@RequestBody Person person) {
return ResponseEntity.created(URI.create("/persons/" + person.getName())).build();
}
}
static class Person {
private final String name;
@JsonCreator
public Person(@JsonProperty("name") String name) {
this.name = name;
}
public String getName() {
return this.name;
}
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (other == null || getClass() != other.getClass()) return false;
Person person = (Person) other;
return getName().equals(person.getName());
}
@Override
public int hashCode() {
return getName().hashCode();
}
@Override
public String toString() {
return "Person[name='" + name + "']";
}
}
}

79
spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ApplicationContextTests.java

@ -0,0 +1,79 @@ @@ -0,0 +1,79 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server.samples.bind;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.config.EnableWebFlux;
/**
* Binding to server infrastructure declared in a Spring ApplicationContext.
*
* @author Rossen Stoyanchev
*/
@SuppressWarnings("unused")
public class ApplicationContextTests {
private WebTestClient client;
@Before
public void setUp() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(WebConfig.class);
context.refresh();
this.client = WebTestClient.bindToApplicationContext(context).build();
}
@Test
public void test() throws Exception {
this.client.get().uri("/test")
.exchange()
.assertStatus().isOk()
.assertEntity(String .class).isEqualTo("It works!");
}
@Configuration
@EnableWebFlux
static class WebConfig {
@Bean
public TestController controller() {
return new TestController();
}
}
@RestController
static class TestController {
@GetMapping("/test")
public String handle() {
return "It works!";
}
}
}

59
spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ControllerTests.java

@ -0,0 +1,59 @@ @@ -0,0 +1,59 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server.samples.bind;
import org.junit.Before;
import org.junit.Test;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Bind to annotated controllers.
*
* @author Rossen Stoyanchev
*/
@SuppressWarnings("unused")
public class ControllerTests {
private WebTestClient client;
@Before
public void setUp() throws Exception {
this.client = WebTestClient.bindToController(new TestController()).build();
}
@Test
public void test() throws Exception {
this.client.get().uri("/test")
.exchange()
.assertStatus().isOk()
.assertEntity(String .class).isEqualTo("It works!");
}
@RestController
static class TestController {
@GetMapping("/test")
public String handle() {
return "It works!";
}
}
}

75
spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/HttpServerTests.java

@ -0,0 +1,75 @@ @@ -0,0 +1,75 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server.samples.bind;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.bootstrap.ReactorHttpServer;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
/**
* Bind to a running server, making actual requests over a socket.
*
* @author Rossen Stoyanchev
*/
public class HttpServerTests {
private ReactorHttpServer server;
private WebTestClient client;
@Before
public void setUp() throws Exception {
HttpHandler httpHandler = RouterFunctions.toHttpHandler(
route(GET("/test"), request ->
ServerResponse.ok().body(Mono.just("It works!"), String.class)));
this.server = new ReactorHttpServer();
this.server.setHandler(httpHandler);
this.server.afterPropertiesSet();
this.server.start();
this.client = WebTestClient.bindToServer()
.baseUrl("http://localhost:" + this.server.getPort())
.build();
}
@After
public void tearDown() throws Exception {
this.server.stop();
}
@Test
public void test() throws Exception {
this.client.get().uri("/test")
.exchange()
.assertStatus().isOk()
.assertEntity(String .class).isEqualTo("It works!");
}
}

56
spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/RouterFunctionTests.java

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
/*
* Copyright 2002-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.web.reactive.server.samples.bind;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
/**
* Bind to a {@link RouterFunction} and functional endpoints.
*
* @author Rossen Stoyanchev
*/
public class RouterFunctionTests {
private WebTestClient testClient;
@Before
public void setUp() throws Exception {
RouterFunction<?> route = route(GET("/test"), request ->
ServerResponse.ok().body(Mono.just("It works!"), String.class));
this.testClient = WebTestClient.bindToRouterFunction(route).build();
}
@Test
public void test() throws Exception {
this.testClient.get().uri("/test")
.exchange()
.assertStatus().isOk()
.assertEntity(String .class).isEqualTo("It works!");
}
}
Loading…
Cancel
Save