(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.
+ * Consider using statically imported methods to improve readability
+ * @param consumer consumer that will apply assertions.
+ */
+ public ExchangeActions andAssert(Consumer consumer) {
+ consumer.accept(this.exchangeInfo);
+ return this;
+ }
+
+ /**
+ * Apply custom actions on the performed exchange.
+ * Consider using statically imported methods to improve readability
+ * @param consumer consumer that will apply the custom action
+ */
+ public ExchangeActions andDo(Consumer consumer) {
+ consumer.accept(this.exchangeInfo);
+ return this;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeInfo.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeInfo.java
new file mode 100644
index 00000000000..a33d211acf2
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeInfo.java
@@ -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());
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java
new file mode 100644
index 00000000000..a6b500159bc
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java
@@ -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.
+ *
+ * Internally the connector uses and adapts
+ * {@link MockClientHttpRequest} and {@link MockClientHttpResponse} to
+ * {@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 connect(HttpMethod httpMethod, URI uri,
+ Function super ClientHttpRequest, Mono> requestCallback) {
+
+ MonoProcessor 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 body) {
+ HttpMethod method = request.getMethod();
+ URI uri = request.getURI();
+ HttpHeaders headers = request.getHeaders();
+ MultiValueMap cookies = request.getCookies();
+ return MockServerHttpRequest.method(method, uri).headers(headers).cookies(cookies).body(body);
+ }
+
+ private ClientHttpResponse adaptResponse(MockServerHttpResponse response, Flux 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;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/LoggingExchangeConsumer.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/LoggingExchangeConsumer.java
new file mode 100644
index 00000000000..65f9e5ef925
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/LoggingExchangeConsumer.java
@@ -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 logLevelPredicate, BiConsumer logAction) {
+ if (logLevelPredicate.test(logger)) {
+ logAction.accept(logger, this.exchangeInfo.toString());
+ }
+ return this.exchangeActions;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/MultiValueMapEntryAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/MultiValueMapEntryAssertions.java
new file mode 100644
index 00000000000..0a085408fc8
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/MultiValueMapEntryAssertions.java
@@ -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 the type of values in the map.
+ *
+ * @author Rossen Stoyanchev
+ * @since 5.0
+ */
+public class MultiValueMapEntryAssertions {
+
+ private final ExchangeActions exchangeActions;
+
+ private final String name;
+
+ private final MultiValueMap map;
+
+ private final String errorMessagePrefix;
+
+
+ MultiValueMapEntryAssertions(ExchangeActions actions, String name,
+ MultiValueMap 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 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 actual = this.map.get(this.name);
+ List 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 getMap() {
+ return this.map;
+ }
+
+ protected String getErrorMessagePrefix() {
+ return this.errorMessagePrefix;
+ }
+
+ protected V getValue(int index) {
+ List actualValues = getMap().get(getName());
+ if (actualValues == null || index >= actualValues.size()) {
+ fail(getErrorMessagePrefix() + " does not have values at index[" + index + "]: " + actualValues);
+ }
+ return actualValues.get(index);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseContentAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseContentAssertions.java
new file mode 100644
index 00000000000..1548d7c7199
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseContentAssertions.java
@@ -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 extends ResponseEntityAssertions {
+
+
+ ResponseContentAssertions(ExchangeInfo exchangeInfo, ResolvableType entityType) {
+ super(exchangeInfo, entityType);
+ }
+
+
+ /**
+ * Assert the response as a collection of entities.
+ */
+ public ResponseEntityCollectionAssertions collection() {
+ return new ResponseEntityCollectionAssertions<>(getExchangeInfo(), getEntityType());
+ }
+
+ /**
+ * Assert the response using a {@link StepVerifier}.
+ */
+ public StepVerifier.FirstStep stepVerifier() {
+ Flux flux = getExchangeInfo().getResponse().body(toFlux(getEntityType()));
+ return StepVerifier.create(flux);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseEntityAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseEntityAssertions.java
new file mode 100644
index 00000000000..d40c72d2fc7
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseEntityAssertions.java
@@ -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 {
+
+ 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 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 isEqualTo(T expected) {
+ assertEquals("Response body", expected, resolveEntity());
+ return this;
+ }
+
+ /**
+ * Assert the response entity is not equal to the given expected entity.
+ */
+ public ResponseEntityAssertions 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.
+ * Consider using statically imported methods for creating the assertion
+ * consumer to improve readability of tests.
+ * @param assertionConsumer consumer that will apply assertions.
+ */
+ public ResponseEntityAssertions andAssert(Consumer assertionConsumer) {
+ assertionConsumer.accept(resolveEntity());
+ return this;
+ }
+
+ /**
+ * Apply custom actions on the response entity collection.
+ * 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 andDo(Consumer consumer) {
+ consumer.accept(resolveEntity());
+ return this;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseEntityCollectionAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseEntityCollectionAssertions.java
new file mode 100644
index 00000000000..047bc43af36
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseEntityCollectionAssertions.java
@@ -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 {
+
+ private final Collection entities;
+
+
+ ResponseEntityCollectionAssertions(ExchangeInfo info, ResolvableType entityType) {
+ Flux flux = info.getResponse().body(toFlux(entityType));
+ this.entities = flux.collectList().block(info.getResponseTimeout());
+ }
+
+
+ /**
+ * Assert the number of entities.
+ */
+ public ResponseEntityCollectionAssertions 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 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 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.
+ * Consider using statically imported methods for creating the assertion
+ * consumer to improve readability of tests.
+ * @param assertionConsumer consumer that will apply assertions.
+ */
+ public ResponseEntityCollectionAssertions andAssert(Consumer> assertionConsumer) {
+ assertionConsumer.accept(this.entities);
+ return this;
+ }
+
+ /**
+ * Apply custom actions on the response entity collection.
+ * 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 andDo(Consumer> consumer) {
+ consumer.accept(this.entities);
+ return this;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseHeadersAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseHeadersAssertions.java
new file mode 100644
index 00000000000..519f5a46101
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseHeadersAssertions.java
@@ -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;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseStatusAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseStatusAssertions.java
new file mode 100644
index 00000000000..387392f4930
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ResponseStatusAssertions.java
@@ -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;
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/StringMultiValueMapEntryAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/StringMultiValueMapEntryAssertions.java
new file mode 100644
index 00000000000..31309921ebf
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/StringMultiValueMapEntryAssertions.java
@@ -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 {
+
+
+ public StringMultiValueMapEntryAssertions(ExchangeActions actions, String name,
+ MultiValueMap 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();
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java
new file mode 100644
index 00000000000..fff068c3a19
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java
@@ -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.
+ *
+ * The {@code WebTestClient} has 3 setup options without a running server:
+ *
+ * - {@link #bindToController}
+ *
- {@link #bindToApplicationContext}
+ *
- {@link #bindToRouterFunction}
+ *
+ * and 1 option for actual requests on a socket:
+ *
+ * - {@link #bindToServer()}
+ *
+ *
+ * @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 consumer);
+
+ /**
+ * Configure CORS support.
+ * @see WebFluxConfigurer#addCorsMappings
+ */
+ ControllerSpec corsMappings(Consumer consumer);
+
+ /**
+ * Configure path matching options.
+ * @see WebFluxConfigurer#configurePathMatching
+ */
+ ControllerSpec pathMatching(Consumer consumer);
+
+ /**
+ * Modify or extend the list of built-in message readers.
+ * @see WebFluxConfigurer#configureMessageReaders
+ */
+ ControllerSpec messageReaders(Consumer>> readers);
+
+ /**
+ * Modify or extend the list of built-in message writers.
+ * @see WebFluxConfigurer#configureMessageWriters
+ */
+ ControllerSpec messageWriters(Consumer>> writers);
+
+ /**
+ * Register formatters and converters to use for type conversion.
+ * @see WebFluxConfigurer#addFormatters
+ */
+ ControllerSpec formatters(Consumer consumer);
+
+ /**
+ * Configure a global Validator.
+ * @see WebFluxConfigurer#getValidator()
+ */
+ ControllerSpec validator(Validator validator);
+
+ /**
+ * Configure view resolution.
+ * @see WebFluxConfigurer#configureViewResolvers
+ */
+ ControllerSpec viewResolvers(Consumer 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 defaultUriVariables);
+
+ /**
+ * Provide a pre-configured {@link UriBuilderFactory} instance. This is
+ * an alternative to and effectively overrides the following:
+ *
+ * - {@link #baseUrl(String)}
+ *
- {@link #defaultUriVariables(Map)}.
+ *
+ * @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.
+ * 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.
+ *
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 uriVariables);
+
+ /**
+ * Build the URI for the request with a {@link UriBuilder} obtained
+ * through the {@link UriBuilderFactory} configured for this client.
+ */
+ HeaderSpec uri(Function 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 cookies);
+
+ /**
+ * Set the value of the {@code If-Modified-Since} header.
+ * 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 the type contained in the body
+ * @return options for asserting the response with
+ */
+ ExchangeActions exchange(BodyInserter 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 the type of the elements contained in the publisher
+ * @param the type of the {@code Publisher}
+ * @return options for asserting the response with
+ */
+ > ExchangeActions exchange(S publisher, Class elementClass);
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java
new file mode 100644
index 00000000000..d5497750f8f
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java
@@ -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> 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 consumer) {
+ this.listeners.add(consumer);
+ }
+
+
+ @Override
+ public Mono connect(HttpMethod method, URI uri,
+ Function super ClientHttpRequest, Mono> requestCallback) {
+
+ AtomicReference 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;
+ }
+ }
+
+}
diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/package-info.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/package-info.java
new file mode 100644
index 00000000000..90489f3f00d
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/package-info.java
@@ -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;
diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java
new file mode 100644
index 00000000000..5a2fdf64573
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ErrorTests.java
@@ -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");
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/HeaderTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/HeaderTests.java
new file mode 100644
index 00000000000..8f47199b941
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/HeaderTests.java
@@ -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 handleHeader(@RequestHeader("h1") String myHeader) {
+ String value = myHeader + "-pong";
+ return ResponseEntity.ok().header("h1", value).build();
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java
new file mode 100644
index 00000000000..ff8327af499
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/ResponseEntityTests.java
@@ -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 getPersons() {
+ return Flux.just(new Person("Jane"), new Person("Jason"), new Person("John"));
+ }
+
+ @GetMapping(produces = "text/event-stream")
+ Flux getPersonStream() {
+ return Flux.intervalMillis(100).onBackpressureBuffer(10).map(index -> new Person("N" + index));
+ }
+
+ @PostMapping
+ ResponseEntity 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 + "']";
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ApplicationContextTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ApplicationContextTests.java
new file mode 100644
index 00000000000..9cfefa29117
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ApplicationContextTests.java
@@ -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!";
+ }
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ControllerTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ControllerTests.java
new file mode 100644
index 00000000000..542e6aacb6a
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ControllerTests.java
@@ -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!";
+ }
+ }
+}
diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/HttpServerTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/HttpServerTests.java
new file mode 100644
index 00000000000..aede9ed6512
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/HttpServerTests.java
@@ -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!");
+ }
+
+}
diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/RouterFunctionTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/RouterFunctionTests.java
new file mode 100644
index 00000000000..49c1dd6cf8d
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/RouterFunctionTests.java
@@ -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!");
+ }
+
+}