org.springframework.test.web.reactive.server.WebTestClient Maven / Gradle / Ivy
Show all versions of spring-test Show documentation
/*
* Copyright 2002-2024 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
*
* https://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.hamcrest.Matcher;
import org.reactivestreams.Publisher;
import org.springframework.context.ApplicationContext;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.http.codec.ClientCodecConfigurer;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.lang.Nullable;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
import org.springframework.web.reactive.config.BlockingExecutionConfigurer;
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.BodyInserters;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.session.WebSessionManager;
import org.springframework.web.util.UriBuilder;
import org.springframework.web.util.UriBuilderFactory;
/**
* Client for testing web servers that uses {@link WebClient} internally to
* perform requests while also providing a fluent API to verify responses.
* This client can connect to any server over HTTP, or to a WebFlux application
* via mock request and response objects.
*
* Use one of the bindToXxx methods to create an instance. For example:
*
* - {@link #bindToController(Object...)}
*
- {@link #bindToRouterFunction(RouterFunction)}
*
- {@link #bindToApplicationContext(ApplicationContext)}
*
- {@link #bindToServer()}
*
- ...
*
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @author Sam Brannen
* @author Michał Rowicki
* @since 5.0
* @see StatusAssertions
* @see HeaderAssertions
* @see JsonPathAssertions
*/
public interface WebTestClient {
/**
* The name of a request header used to assign a unique id to every request
* performed through the {@code WebTestClient}. This can be useful for
* storing contextual information at all phases of request processing (e.g.
* from a server-side component) under that id and later to look up
* that information once an {@link ExchangeResult} is available.
*/
String WEBTESTCLIENT_REQUEST_ID = "WebTestClient-Request-Id";
/**
* Prepare an HTTP GET request.
* @return a spec for specifying the target URL
*/
RequestHeadersUriSpec get();
/**
* Prepare an HTTP HEAD request.
* @return a spec for specifying the target URL
*/
RequestHeadersUriSpec head();
/**
* Prepare an HTTP POST request.
* @return a spec for specifying the target URL
*/
RequestBodyUriSpec post();
/**
* Prepare an HTTP PUT request.
* @return a spec for specifying the target URL
*/
RequestBodyUriSpec put();
/**
* Prepare an HTTP PATCH request.
* @return a spec for specifying the target URL
*/
RequestBodyUriSpec patch();
/**
* Prepare an HTTP DELETE request.
* @return a spec for specifying the target URL
*/
RequestHeadersUriSpec delete();
/**
* Prepare an HTTP OPTIONS request.
* @return a spec for specifying the target URL
*/
RequestHeadersUriSpec options();
/**
* Prepare a request for the specified {@code HttpMethod}.
* @return a spec for specifying the target URL
*/
RequestBodyUriSpec method(HttpMethod method);
/**
* Return a builder to mutate properties of this web test client.
*/
Builder mutate();
/**
* Mutate the {@link WebTestClient}, apply the given configurer, and build
* a new instance. Essentially a shortcut for:
*
* mutate().apply(configurer).build();
*
* @param configurer the configurer to apply
* @return the mutated test client
*/
WebTestClient mutateWith(WebTestClientConfigurer configurer);
// Static factory methods
/**
* Use this server setup to test one {@code @Controller} at a time.
* This option loads the default configuration of
* {@link org.springframework.web.reactive.config.EnableWebFlux @EnableWebFlux}.
* There are builder methods to customize the Java config. The resulting
* WebFlux application will be tested without an HTTP server using a mock
* request and response.
* @param controllers one or more controller instances to test
* (specified {@code Class} will be turned into instance)
* @return chained API to customize server and client config; use
* {@link MockServerSpec#configureClient()} to transition to client config
*/
static ControllerSpec bindToController(Object... controllers) {
return new DefaultControllerSpec(controllers);
}
/**
* Use this option to set up a server from a {@link RouterFunction}.
* Internally the provided configuration is passed to
* {@code RouterFunctions#toWebHandler}. The resulting WebFlux application
* will be tested without an HTTP server using a mock request and response.
* @param routerFunction the RouterFunction to test
* @return chained API to customize server and client config; use
* {@link MockServerSpec#configureClient()} to transition to client config
*/
static RouterFunctionSpec bindToRouterFunction(RouterFunction routerFunction) {
return new DefaultRouterFunctionSpec(routerFunction);
}
/**
* Use this option to set up a server from the Spring configuration of your
* application, or some subset of it. Internally the provided configuration
* is passed to {@code WebHttpHandlerBuilder} to set up the request
* processing chain. The resulting WebFlux application will be tested
* without an HTTP server using a mock request and response.
* Consider using the TestContext framework and
* {@link org.springframework.test.context.ContextConfiguration @ContextConfiguration}
* in order to efficiently load and inject the Spring configuration into the
* test class.
* @param applicationContext the Spring context
* @return chained API to customize server and client config; use
* {@link MockServerSpec#configureClient()} to transition to client config
*/
static MockServerSpec bindToApplicationContext(ApplicationContext applicationContext) {
return new ApplicationContextSpec(applicationContext);
}
/**
* Integration testing with a "mock" server targeting the given WebHandler.
* @param webHandler the handler to test
* @return chained API to customize server and client config; use
* {@link MockServerSpec#configureClient()} to transition to client config
*/
static MockServerSpec bindToWebHandler(WebHandler webHandler) {
return new DefaultMockServerSpec(webHandler);
}
/**
* This server setup option allows you to connect to a live server through
* a Reactor Netty client connector.
*
* WebTestClient client = WebTestClient.bindToServer()
* .baseUrl("http://localhost:8080")
* .build();
*
* @return chained API to customize client config
*/
static Builder bindToServer() {
return new DefaultWebTestClientBuilder();
}
/**
* A variant of {@link #bindToServer()} with a pre-configured connector.
* @return chained API to customize client config
* @since 5.0.2
*/
static Builder bindToServer(ClientHttpConnector connector) {
return new DefaultWebTestClientBuilder(connector);
}
/**
* Base specification for setting up tests without a server.
*
* @param a self reference to the builder type
*/
interface MockServerSpec> {
/**
* Register {@link WebFilter} instances to add to the mock server.
* @param filter one or more filters
*/
T webFilter(WebFilter... filter);
/**
* Provide a session manager instance for the mock server.
* By default an instance of
* {@link org.springframework.web.server.session.DefaultWebSessionManager
* DefaultWebSessionManager} is used.
* @param sessionManager the session manager to use
*/
T webSessionManager(WebSessionManager sessionManager);
/**
* Shortcut for pre-packaged customizations to the mock server setup.
* @param configurer the configurer to apply
*/
T apply(MockServerConfigurer configurer);
/**
* Proceed to configure and build the test client.
*/
Builder configureClient();
/**
* Shortcut to build the test client.
*/
WebTestClient build();
}
/**
* Specification for customizing controller configuration equivalent to, and
* internally delegating to, a {@link WebFluxConfigurer}.
*/
interface ControllerSpec extends MockServerSpec {
/**
* Register one or more {@link org.springframework.web.bind.annotation.ControllerAdvice}
* instances to be used in tests (specified {@code Class} will be turned into instance).
*/
ControllerSpec controllerAdvice(Object... controllerAdvice);
/**
* 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);
/**
* Configure resolvers for custom controller method arguments.
* @see WebFluxConfigurer#configureHttpMessageCodecs
*/
ControllerSpec argumentResolvers(Consumer configurer);
/**
* Configure custom HTTP message readers and writers or override built-in ones.
* @see WebFluxConfigurer#configureHttpMessageCodecs
*/
ControllerSpec httpMessageCodecs(Consumer configurer);
/**
* 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);
/**
* Configure blocking execution options.
* @since 6.1
* @see WebFluxConfigurer#configureBlockingExecution
*/
ControllerSpec blockingExecution(Consumer consumer);
}
/**
* Specification for customizing router function configuration.
*/
interface RouterFunctionSpec extends MockServerSpec {
/**
* Configure handler strategies.
*/
RouterFunctionSpec handlerStrategies(HandlerStrategies handlerStrategies);
}
/**
* Steps for customizing the {@link WebClient} used to test with,
* internally delegating to a
* {@link org.springframework.web.reactive.function.client.WebClient.Builder
* WebClient.Builder}.
*/
interface Builder {
/**
* Configure a base URI as described in
* {@link org.springframework.web.reactive.function.client.WebClient#create(String)
* WebClient.create(String)}.
*/
Builder baseUrl(String baseUrl);
/**
* Provide a pre-configured {@link UriBuilderFactory} instance as an
* alternative to and effectively overriding {@link #baseUrl(String)}.
*/
Builder 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
*/
Builder defaultHeader(String headerName, String... headerValues);
/**
* Manipulate the default headers with the given consumer. The
* headers provided to the consumer are "live", so that the consumer can be used to
* {@linkplain HttpHeaders#set(String, String) overwrite} existing header values,
* {@linkplain HttpHeaders#remove(Object) remove} values, or use any of the other
* {@link HttpHeaders} methods.
* @param headersConsumer a function that consumes the {@code HttpHeaders}
* @return this builder
*/
Builder defaultHeaders(Consumer headersConsumer);
/**
* Add the given header to all requests that haven't added it.
* @param cookieName the cookie name
* @param cookieValues the cookie values
*/
Builder defaultCookie(String cookieName, String... cookieValues);
/**
* Manipulate the default cookies with the given consumer. The
* map provided to the consumer is "live", so that the consumer can be used to
* {@linkplain MultiValueMap#set(Object, Object) overwrite} existing header values,
* {@linkplain MultiValueMap#remove(Object) remove} values, or use any of the other
* {@link MultiValueMap} methods.
* @param cookiesConsumer a function that consumes the cookies map
* @return this builder
*/
Builder defaultCookies(Consumer> cookiesConsumer);
/**
* Add the given filter to the filter chain.
* @param filter the filter to be added to the chain
*/
Builder filter(ExchangeFilterFunction filter);
/**
* Manipulate the filters with the given consumer. The
* list provided to the consumer is "live", so that the consumer can be used to remove
* filters, change ordering, etc.
* @param filtersConsumer a function that consumes the filter list
* @return this builder
*/
Builder filters(Consumer> filtersConsumer);
/**
* Configure an {@code EntityExchangeResult} callback that is invoked
* every time after a response is fully decoded to a single entity, to a
* List of entities, or to a byte[]. In effect, equivalent to each and
* all of the below but registered once, globally:
*
* client.get().uri("/accounts/1")
* .exchange()
* .expectBody(Person.class).consumeWith(exchangeResult -> ... ));
*
* client.get().uri("/accounts")
* .exchange()
* .expectBodyList(Person.class).consumeWith(exchangeResult -> ... ));
*
* client.get().uri("/accounts/1")
* .exchange()
* .expectBody().consumeWith(exchangeResult -> ... ));
*
* Note that the configured consumer does not apply to responses
* decoded to {@code Flux} which can be consumed outside the workflow
* of the test client, for example via {@code reactor.test.StepVerifier}.
* @param consumer the consumer to apply to entity responses
* @return the builder
* @since 5.3.5
*/
Builder entityExchangeResultConsumer(Consumer> consumer);
/**
* Configure the codecs for the {@code WebClient} in the
* {@link #exchangeStrategies(ExchangeStrategies) underlying}
* {@code ExchangeStrategies}.
* @param configurer the configurer to apply
* @since 5.1.13
*/
Builder codecs(Consumer configurer);
/**
* Configure the {@link ExchangeStrategies} to use.
* For most cases, prefer using {@link #codecs(Consumer)} which allows
* customizing the codecs in the {@code ExchangeStrategies} rather than
* replace them. That ensures multiple parties can contribute to codecs
* configuration.
*
By default this is set to {@link ExchangeStrategies#withDefaults()}.
* @param strategies the strategies to use
*/
Builder exchangeStrategies(ExchangeStrategies strategies);
/**
* Customize the strategies configured via
* {@link #exchangeStrategies(ExchangeStrategies)}. This method is
* designed for use in scenarios where multiple parties wish to update
* the {@code ExchangeStrategies}.
* @deprecated as of 5.1.13 in favor of {@link #codecs(Consumer)}
*/
@Deprecated
Builder exchangeStrategies(Consumer configurer);
/**
* Max amount of time to wait for responses.
* By default 5 seconds.
* @param timeout the response timeout value
*/
Builder responseTimeout(Duration timeout);
/**
* Set the {@link ClientHttpConnector} to use.
*
By default, this is initialized and set internally. However, the
* connector may also be prepared externally and passed via
* {@link WebTestClient#bindToServer(ClientHttpConnector)} such as for
* {@code MockMvcWebTestClient} tests, and in that case you can use this
* from {@link #mutateWith(WebTestClientConfigurer)} to replace it.
* @param connector the connector to use
* @since 6.1
*/
Builder clientConnector(ClientHttpConnector connector);
/**
* Apply the given configurer to this builder instance.
*
This can be useful for applying pre-packaged customizations.
* @param configurer the configurer to apply
*/
Builder apply(WebTestClientConfigurer configurer);
/**
* Build the {@link WebTestClient} instance.
*/
WebTestClient build();
}
/**
* Specification for providing the URI of a request.
*
* @param a self reference to the spec type
*/
interface UriSpec> {
/**
* Specify the URI using an absolute, fully constructed {@link java.net.URI}.
*
If a {@link UriBuilderFactory} was configured for the client with
* a base URI, that base URI will not be applied to the
* supplied {@code java.net.URI}. If you wish to have a base URI applied to a
* {@code java.net.URI} you must invoke either {@link #uri(String, Object...)}
* or {@link #uri(String, Map)} — for example, {@code uri(myUri.toString())}.
* @return spec to add headers or perform the exchange
*/
S 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.
* @return spec to add headers or perform the exchange
*/
S 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.
* @return spec to add headers or perform the exchange
*/
S uri(String uri, Map uriVariables);
/**
* Build the URI for the request with a {@link UriBuilder} obtained
* through the {@link UriBuilderFactory} configured for this client.
* @return spec to add headers or perform the exchange
*/
S uri(Function uriFunction);
}
/**
* Specification for adding request headers and performing an exchange.
*
* @param a self reference to the spec type
*/
interface RequestHeadersSpec> {
/**
* Set the list of acceptable {@linkplain MediaType media types}, as
* specified by the {@code Accept} header.
* @param acceptableMediaTypes the acceptable media types
* @return the same instance
*/
S 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 the same instance
*/
S acceptCharset(Charset... acceptableCharsets);
/**
* Add a cookie with the given name and value.
* @param name the cookie name
* @param value the cookie value
* @return the same instance
*/
S cookie(String name, String value);
/**
* Manipulate this request's cookies with the given consumer. The
* map provided to the consumer is "live", so that the consumer can be used to
* {@linkplain MultiValueMap#set(Object, Object) overwrite} existing header values,
* {@linkplain MultiValueMap#remove(Object) remove} values, or use any of the other
* {@link MultiValueMap} methods.
* @param cookiesConsumer a function that consumes the cookies map
* @return this builder
*/
S cookies(Consumer> cookiesConsumer);
/**
* 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 the same instance
*/
S ifModifiedSince(ZonedDateTime ifModifiedSince);
/**
* Set the values of the {@code If-None-Match} header.
* @param ifNoneMatches the new value of the header
* @return the same instance
*/
S 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 the same instance
*/
S header(String headerName, String... headerValues);
/**
* Manipulate the request's headers with the given consumer. The
* headers provided to the consumer are "live", so that the consumer can be used to
* {@linkplain HttpHeaders#set(String, String) overwrite} existing header values,
* {@linkplain HttpHeaders#remove(Object) remove} values, or use any of the other
* {@link HttpHeaders} methods.
* @param headersConsumer a function that consumes the {@code HttpHeaders}
* @return this builder
*/
S headers(Consumer headersConsumer);
/**
* Set the attribute with the given name to the given value.
* @param name the name of the attribute to add
* @param value the value of the attribute to add
* @return this builder
*/
S attribute(String name, Object value);
/**
* Manipulate the request attributes with the given consumer. The attributes provided to
* the consumer are "live", so that the consumer can be used to inspect attributes,
* remove attributes, or use any of the other map-provided methods.
* @param attributesConsumer a function that consumes the attributes
* @return this builder
*/
S attributes(Consumer