Spring Framework
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

443 lines
12 KiB

[[webtestclient]]
= WebTestClient
`WebTestClient` is a thin shell around <<web-reactive.adoc#webflux-client, WebClient>>,
using it to perform requests and exposing a dedicated, fluent API for verifying responses.
`WebTestClient` binds to a WebFlux application by using a
<<testing.adoc#mock-objects-web-reactive, mock request and response>>, or it can test any
web server over an HTTP connection.
TIP: Kotlin users: See <<languages.adoc#kotlin-webtestclient-issue, this section>>
related to use of the `WebTestClient`.
[[webtestclient-setup]]
== Setup
To create a `WebTestClient` you must choose one of several server setup options.
Effectively you're either configuring the WebFlux application to bind to or using
a URL to connect to a running server.
[[webtestclient-controller-config]]
=== Bind to Controller
The following example shows how to create a server setup to test one `@Controller` at a time:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
client = WebTestClient.bindToController(new TestController()).build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
client = WebTestClient.bindToController(TestController()).build()
----
The preceding example loads the <<web-reactive.adoc#webflux-config, WebFlux Java configuration>>
and registers the given controller. The resulting WebFlux application is tested
without an HTTP server by using mock request and response objects. There are more methods
on the builder to customize the default WebFlux Java configuration.
[[webtestclient-fn-config]]
=== Bind to Router Function
The following example shows how to set up a server from a
<<web-reactive.adoc#webflux-fn, RouterFunction>>:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
RouterFunction<?> route = ...
client = WebTestClient.bindToRouterFunction(route).build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val route: RouterFunction<*> = ...
val client = WebTestClient.bindToRouterFunction(route).build()
----
Internally, the configuration is passed to `RouterFunctions.toWebHandler`.
The resulting WebFlux application is tested without an HTTP server by using mock
request and response objects.
[[webtestclient-context-config]]
=== Bind to `ApplicationContext`
The following example shows how to set up a server from the Spring configuration of your
application or some subset of it:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@SpringJUnitConfig(WebConfig.class) // <1>
class MyTests {
WebTestClient client;
@BeforeEach
void setUp(ApplicationContext context) { // <2>
client = WebTestClient.bindToApplicationContext(context).build(); // <3>
}
}
----
<1> Specify the configuration to load
<2> Inject the configuration
<3> Create the `WebTestClient`
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
@SpringJUnitConfig(WebConfig::class) // <1>
class MyTests {
lateinit var client: WebTestClient
@BeforeEach
fun setUp(context: ApplicationContext) { // <2>
client = WebTestClient.bindToApplicationContext(context).build() // <3>
}
}
----
<1> Specify the configuration to load
<2> Inject the configuration
<3> Create the `WebTestClient`
Internally, the 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 is tested without an HTTP server by
using mock request and response objects.
[[webtestclient-server-config]]
=== Bind to Server
The following server setup option lets you connect to a running server:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build()
----
[[webtestclient-client-config]]
=== Client Builder
In addition to the server setup options described earlier, 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 follows:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
client = WebTestClient.bindToController(new TestController())
.configureClient()
.baseUrl("/test")
.build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
client = WebTestClient.bindToController(TestController())
.configureClient()
.baseUrl("/test")
.build()
----
[[webtestclient-tests]]
== Writing Tests
`WebTestClient` provides an API identical to <<web-reactive.adoc#webflux-client, WebClient>>
up to the point of performing a request by using `exchange()`. What follows after
`exchange()` is a chained API workflow to verify responses.
Typically, you start by asserting the response status and headers, as follows:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
----
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 an empty body.
Then you can use built-in assertions for the body. The following example shows one way to do so:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBodyList(Person.class).hasSize(3).contains(person);
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
import org.springframework.test.web.reactive.server.expectBodyList
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBodyList<Person>().hasSize(3).contains(person)
----
You can also go beyond the built-in assertions and create your own, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
import org.springframework.test.web.reactive.server.expectBody
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.consumeWith(result -> {
// custom assertions (e.g. AssertJ)...
});
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody<Person>()
.consumeWith {
// custom assertions (e.g. AssertJ)...
}
----
You can also exit the workflow and get a result, as follows:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
import org.springframework.test.web.reactive.server.expectBody
val result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk
.expectBody<Person>()
.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>`.
[[webtestclient-no-content]]
=== No Content
If the response has no content (or you do not care if it does) use `Void.class`, which ensures
that resources are released. The following example shows how to do so:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound()
.expectBody(Void.class);
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound
.expectBody<Unit>()
----
Alternatively, if you want to assert there is no response content, you can use code similar to the following:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
client.post().uri("/persons")
.body(personMono, Person.class)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
client.post().uri("/persons")
.bodyValue(person)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty()
----
[[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
https://jsonassert.skyscreamer.org[JSONAssert] to verify JSON content, as follows:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")
----
You can also use https://github.com/jayway/JsonPath[JSONPath] expressions, as follows:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason");
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
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 (for example, `"text/event-stream"` or `"application/stream+json"`),
you need to exit the chained API (by using `returnResult`), immediately after the response status
and header assertions, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
FluxExchangeResult<MyEvent> result = client.get().uri("/events")
.accept(TEXT_EVENT_STREAM)
.exchange()
.expectStatus().isOk()
.returnResult(MyEvent.class);
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
import org.springframework.test.web.reactive.server.returnResult
val result = client.get().uri("/events")
.accept(TEXT_EVENT_STREAM)
.exchange()
.expectStatus().isOk()
.returnResult<MyEvent>()
----
Now you can consume the `Flux<T>`, assert decoded objects as they come, and then
cancel at some point when test objectives are met. We recommend using the `StepVerifier`
from the `reactor-test` module to do that, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
Flux<Event> eventFlux = result.getResponseBody();
StepVerifier.create(eventFlux)
.expectNext(person)
.expectNextCount(4)
.consumeNextWith(p -> ...)
.thenCancel()
.verify();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val eventFlux = result.getResponseBody()
StepVerifier.create(eventFlux)
.expectNext(person)
.expectNextCount(4)
.consumeNextWith { p -> ... }
.thenCancel()
.verify()
----
[[webtestclient-request-body]]
=== Request Body
When it comes to building requests, the `WebTestClient` offers an API identical to
the `WebClient`, and the implementation is mostly a simple pass-through. See the
<<web-reactive.adoc#webflux-client-body, WebClient documentation>> for examples on
how to prepare a request with a body, including submitting form data, multipart requests,
and more.