2017-10-03 21:56:13 +08:00
|
|
|
[[webtestclient]]
|
|
|
|
|
= WebTestClient
|
|
|
|
|
|
|
|
|
|
`WebTestClient` is a non-blocking, reactive client for testing web servers. It uses
|
|
|
|
|
the reactive <<web-reactive.adoc#webflux-webclient, WebClient>> internally to perform
|
|
|
|
|
requests and provides a fluent API to verify responses. The `WebTestClient` can connect
|
|
|
|
|
to any server over an HTTP connection. It can also bind directly to WebFlux applications
|
|
|
|
|
with <<testing.adoc#mock-objects-web-reactive,mock request and response>> objects,
|
|
|
|
|
without the need for an HTTP server.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[[webtestclient-setup]]
|
|
|
|
|
== Setup
|
|
|
|
|
|
|
|
|
|
To create a `WebTestClient` you must choose one of several server setup options.
|
|
|
|
|
Effectively you either configure a WebFlux application to bind to, or use absolute URLs
|
|
|
|
|
to connect to a running server.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[[webtestclient-controller-config]]
|
|
|
|
|
=== Bind to controller
|
|
|
|
|
|
|
|
|
|
Use this server setup to test one `@Controller` at a time:
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
|
----
|
|
|
|
|
client = WebTestClient.bindToController(new TestController()).build();
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
The above loads the <<web-reactive.adoc#webflux-config,WebFlux Java config>> and
|
|
|
|
|
registers the given controller. The resulting WebFlux application will be tested
|
|
|
|
|
without an HTTP server using mock request and response objects. There are more methods
|
|
|
|
|
on the builder to customize the default WebFlux Java config.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[[webtestclient-fn-config]]
|
|
|
|
|
=== Bind to RouterFunction
|
|
|
|
|
|
|
|
|
|
Use this option to set up a server from a
|
|
|
|
|
<<web-reactive.adoc#webflux-fn,RouterFunction>>:
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
|
----
|
|
|
|
|
RouterFunction<?> route = ...
|
|
|
|
|
client = WebTestClient.bindToRouterFunction(route).build();
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
Internally the provided configuration is passed to `RouterFunctions.toWebHandler`.
|
|
|
|
|
The resulting WebFlux application will be tested without an HTTP server using mock
|
2017-10-05 02:58:11 +08:00
|
|
|
request and response objects.
|
2017-10-03 21:56:13 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
[[webtestclient-context-config]]
|
|
|
|
|
=== Bind to ApplicationContext
|
|
|
|
|
|
|
|
|
|
Use this option to setup a server from the Spring configuration of your application, or
|
|
|
|
|
some subset of it:
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
|
----
|
|
|
|
|
@RunWith(SpringRunner.class)
|
|
|
|
|
@ContextConfiguration(classes = WebConfig.class) // <1>
|
|
|
|
|
public class MyTests {
|
|
|
|
|
|
|
|
|
|
@Autowired
|
|
|
|
|
private ApplicationContext context; // <2>
|
|
|
|
|
|
|
|
|
|
private WebTestClient client;
|
|
|
|
|
|
|
|
|
|
@Before
|
|
|
|
|
public void setUp() {
|
|
|
|
|
client = WebTestClient.bindToApplicationContext(context).build(); // <3>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
<1> Specify the configuration to load
|
|
|
|
|
<2> Inject the configuration
|
|
|
|
|
<3> Create the `WebTestClient`
|
|
|
|
|
|
|
|
|
|
Internally the provided configuration is passed to `WebHttpHandlerBuilder` to set up
|
|
|
|
|
the request processing chain, see
|
|
|
|
|
<<web-reactive.adoc#webflux-web-handler-api,WebHandler API>> for more details. The
|
|
|
|
|
resulting WebFlux application will be tested without an HTTP server using mock request
|
|
|
|
|
and response objects.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[[webtestclient-server-config]]
|
|
|
|
|
=== Bind to server
|
|
|
|
|
|
|
|
|
|
This server setup option allows you to connect to a running server:
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
|
----
|
|
|
|
|
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[[webtestclient-client-config]]
|
|
|
|
|
=== Client builder
|
|
|
|
|
|
|
|
|
|
In addition to the server setup options above, you can also configure client
|
|
|
|
|
options including base URL, default headers, client filters, and others. These options
|
|
|
|
|
are readily available following `bindToServer`. For all others, you need to use
|
|
|
|
|
`configureClient()` to transition from server to client configuration as shown below:
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
|
----
|
|
|
|
|
client = WebTestClient.bindToController(new TestController())
|
|
|
|
|
.configureClient()
|
|
|
|
|
.baseUrl("/test")
|
|
|
|
|
.build();
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[[webtestclient-tests]]
|
|
|
|
|
== Writing tests
|
|
|
|
|
|
|
|
|
|
`WebTestClient` is a thin shell around <<web-reactive.adoc#webflux-webclient,WebClient>>.
|
|
|
|
|
It provides an identical API up to the point of performing a request via `exchange()`.
|
|
|
|
|
What follows after `exchange()` is a chained API workflow to verify responses.
|
|
|
|
|
|
|
|
|
|
Typically you start by asserting the response status and headers:
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
|
----
|
|
|
|
|
client.get().uri("/persons/1")
|
|
|
|
|
.accept(MediaType.APPLICATION_JSON_UTF8)
|
|
|
|
|
.exchange()
|
|
|
|
|
.expectStatus().isOk()
|
|
|
|
|
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
|
|
|
|
|
// ...
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
Then you specify how to decode and consume the response body:
|
|
|
|
|
|
|
|
|
|
* `expectBody(Class<T>)` -- decode to single object.
|
|
|
|
|
* `expectBodyList(Class<T>)` -- decode and collect objects to `List<T>`.
|
|
|
|
|
* `expectBody()` -- decode to `byte[]` for <<webtestclient-json>> or empty body.
|
|
|
|
|
|
|
|
|
|
Then you can use built-in assertions for the body. Here is one example:
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
|
----
|
|
|
|
|
client.get().uri("/persons")
|
|
|
|
|
.exchange()
|
|
|
|
|
.expectStatus().isOk()
|
|
|
|
|
.expectBodyList(Person.class).hasSize(3).contains(person);
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
You can go beyond the built-in assertions and create your own:
|
|
|
|
|
|
|
|
|
|
----
|
|
|
|
|
client.get().uri("/persons/1")
|
|
|
|
|
.exchange()
|
|
|
|
|
.expectStatus().isOk()
|
|
|
|
|
.expectBody(Person.class)
|
|
|
|
|
.consumeWith(result -> {
|
|
|
|
|
// custom assertions (e.g. AssertJ)...
|
|
|
|
|
});
|
|
|
|
|
----
|
|
|
|
|
|
2017-10-05 02:58:11 +08:00
|
|
|
You can also exit the workflow and get a result:
|
2017-10-03 21:56:13 +08:00
|
|
|
|
|
|
|
|
----
|
|
|
|
|
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
|
|
|
|
|
.exchange()
|
|
|
|
|
.expectStatus().isOk()
|
|
|
|
|
.expectBody(Person.class)
|
|
|
|
|
.returnResult();
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
[TIP]
|
|
|
|
|
====
|
|
|
|
|
When you need to decode to a target type with generics, look for the overloaded methods
|
|
|
|
|
that accept
|
|
|
|
|
{api-spring-framework}/core/ParameterizedTypeReference.html[ParameterizedTypeReference]
|
|
|
|
|
instead of `Class<T>`.
|
|
|
|
|
====
|
|
|
|
|
|
|
|
|
|
|
2017-10-05 02:58:11 +08:00
|
|
|
[[webtestclient-no-content]]
|
|
|
|
|
=== No content
|
|
|
|
|
|
|
|
|
|
If the response has no content, or you don't care if it does, use `Void.class` which ensures
|
|
|
|
|
that resources are released:
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
|
----
|
|
|
|
|
client.get().uri("/persons/123")
|
|
|
|
|
.exchange()
|
|
|
|
|
.expectStatus().isNotFound()
|
|
|
|
|
.expectBody(Void.class);
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
Or if you want to assert there is no response content, use this:
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
|
----
|
|
|
|
|
client.post().uri("/persons")
|
|
|
|
|
.body(personMono, Person.class)
|
|
|
|
|
.exchange()
|
|
|
|
|
.expectStatus().isCreated()
|
|
|
|
|
.expectBody().isEmpty;
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2017-10-03 21:56:13 +08:00
|
|
|
[[webtestclient-json]]
|
|
|
|
|
=== JSON content
|
|
|
|
|
|
|
|
|
|
When you use `expectBody()` the response is consumed as a `byte[]`. This is useful for
|
|
|
|
|
raw content assertions. For example you can use
|
|
|
|
|
http://jsonassert.skyscreamer.org[JSONAssert] to verify JSON content:
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
|
----
|
|
|
|
|
client.get().uri("/persons/1")
|
|
|
|
|
.exchange()
|
|
|
|
|
.expectStatus().isOk()
|
|
|
|
|
.expectBody()
|
|
|
|
|
.json("{\"name\":\"Jane\"}")
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
You can also use https://github.com/jayway/JsonPath[JSONPath] expressions:
|
|
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
|
----
|
|
|
|
|
client.get().uri("/persons")
|
|
|
|
|
.exchange()
|
|
|
|
|
.expectStatus().isOk()
|
|
|
|
|
.expectBody()
|
|
|
|
|
.jsonPath("$[0].name").isEqualTo("Jane")
|
|
|
|
|
.jsonPath("$[1].name").isEqualTo("Jason");
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[[webtestclient-stream]]
|
|
|
|
|
=== Streaming responses
|
|
|
|
|
|
|
|
|
|
To test infinite streams (e.g. `"text/event-stream"`, `"application/stream+json"`),
|
2017-10-05 02:58:11 +08:00
|
|
|
you'll need to exit the chained API, via `returnResult`, immediately after response status
|
|
|
|
|
and header assertions, as shown below:
|
2017-10-03 21:56:13 +08:00
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
|
----
|
2017-10-05 02:58:11 +08:00
|
|
|
FluxExchangeResult<MyEvent> result = client.get().uri("/events")
|
2017-10-03 21:56:13 +08:00
|
|
|
.accept(TEXT_EVENT_STREAM)
|
|
|
|
|
.exchange()
|
|
|
|
|
.expectStatus().isOk()
|
2017-10-05 02:58:11 +08:00
|
|
|
.returnResult(MyEvent.class);
|
2017-10-03 21:56:13 +08:00
|
|
|
|
|
|
|
|
----
|
|
|
|
|
|
2017-10-05 02:58:11 +08:00
|
|
|
Now you can consume the `Flux<T>`, assert decoded objects as they come, and then
|
|
|
|
|
cancel at some point when test objects are met. We recommend using the `StepVerifier`
|
|
|
|
|
from the `reactor-test` module to do that, for example:
|
2017-10-03 21:56:13 +08:00
|
|
|
|
|
|
|
|
[source,java,intent=0]
|
|
|
|
|
[subs="verbatim,quotes"]
|
|
|
|
|
----
|
|
|
|
|
Flux<Event> eventFux = result.getResponseBody();
|
|
|
|
|
|
|
|
|
|
StepVerifier.create(eventFlux)
|
|
|
|
|
.expectNext(person)
|
|
|
|
|
.expectNextCount(4)
|
|
|
|
|
.consumeNextWith(p -> ...)
|
|
|
|
|
.thenCancel()
|
|
|
|
|
.verify();
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|