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.
594 lines
16 KiB
594 lines
16 KiB
[[webtestclient]] |
|
= WebTestClient |
|
|
|
`WebTestClient` is an HTTP client designed for testing server applications. It wraps |
|
Spring's <<web-reactive.adoc#webflux-client, WebClient>> and uses it to perform requests |
|
but exposes a testing facade for verifying responses. `WebTestClient` can be used to |
|
perform end-to-end HTTP tests. It can also be used to test Spring MVC and Spring WebFlux |
|
applications without a running server via mock server request and response objects. |
|
|
|
TIP: Kotlin users: See <<languages.adoc#kotlin-webtestclient-issue, this section>> |
|
related to use of the `WebTestClient`. |
|
|
|
|
|
|
|
|
|
[[webtestclient-setup]] |
|
== Setup |
|
|
|
To set up a `WebTestClient` you need to choose a server setup to bind to. This can be one |
|
of several mock server setup choices or a connection to a live server. |
|
|
|
|
|
|
|
[[webtestclient-controller-config]] |
|
=== Bind to Controller |
|
|
|
This setup allows you to test specific controller(s) via mock request and response objects, |
|
without a running server. |
|
|
|
For WebFlux applications, use the following which loads infrastructure equivalent to the |
|
<<web-reactive.adoc#webflux-config, WebFlux Java config>>, registers the given |
|
controller(s), and creates a <<web-reactive.adoc#webflux-web-handler-api, WebHandler chain>> |
|
to handle requests: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
.Java |
|
---- |
|
WebTestClient client = |
|
WebTestClient.bindToController(new TestController()).build(); |
|
---- |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
.Kotlin |
|
---- |
|
val client = WebTestClient.bindToController(TestController()).build() |
|
---- |
|
|
|
For Spring MVC, use the following which delegates to the |
|
{api-spring-framework}/test/web/servlet/setup/StandaloneMockMvcBuilder.html[StandaloneMockMvcBuilder] |
|
to load infrastructure equivalent to the <<web.adoc#mvc-config, WebMvc Java config>>, |
|
registers the given controller(s), and creates an instance of |
|
<<testing.adoc#spring-mvc-test-framework, MockMvc>> to handle requests: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
.Java |
|
---- |
|
WebTestClient client = |
|
MockMvcWebTestClient.bindToController(new TestController()).build(); |
|
---- |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
.Kotlin |
|
---- |
|
val client = MockMvcWebTestClient.bindToController(TestController()).build() |
|
---- |
|
|
|
|
|
|
|
[[webtestclient-context-config]] |
|
=== Bind to `ApplicationContext` |
|
|
|
This setup allows you to load Spring configuration with Spring MVC or Spring WebFlux |
|
infrastructure and controller declarations and use it to handle requests via mock request |
|
and response objects, without a running server. |
|
|
|
For WebFlux, use the following where the Spring `ApplicationContext` is passed to |
|
{api-spring-framework}/web/server/adapter/WebHttpHandlerBuilder.html#applicationContext-org.springframework.context.ApplicationContext-[WebHttpHandlerBuilder] |
|
to create the <<web-reactive.adoc#webflux-web-handler-api, WebHandler chain>> to handle |
|
requests: |
|
|
|
[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` |
|
|
|
For Spring MVC, use the following where the Spring `ApplicationContext` is passed to |
|
{api-spring-framework}/test/web/servlet/setup/MockMvcBuilders.html#webAppContextSetup-org.springframework.web.context.WebApplicationContext-[MockMvcBuilders.webAppContextSetup] |
|
to create a <<testing.adoc#spring-mvc-test-framework, MockMvc>> instance to handle |
|
requests: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
.Java |
|
---- |
|
@ExtendWith(SpringExtension.class) |
|
@WebAppConfiguration("classpath:META-INF/web-resources") // <1> |
|
@ContextHierarchy({ |
|
@ContextConfiguration(classes = RootConfig.class), |
|
@ContextConfiguration(classes = WebConfig.class) |
|
}) |
|
class MyTests { |
|
|
|
@Autowired |
|
WebApplicationContext wac; // <2> |
|
|
|
WebTestClient client; |
|
|
|
@BeforeEach |
|
void setUp() { |
|
client = MockMvcWebTestClient.bindToApplicationContext(this.wac).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 |
|
---- |
|
@ExtendWith(SpringExtension.class) |
|
@WebAppConfiguration("classpath:META-INF/web-resources") // <1> |
|
@ContextHierarchy({ |
|
@ContextConfiguration(classes = RootConfig.class), |
|
@ContextConfiguration(classes = WebConfig.class) |
|
}) |
|
class MyTests { |
|
|
|
@Autowired |
|
lateinit var wac: WebApplicationContext; // <2> |
|
|
|
lateinit var client: WebTestClient |
|
|
|
@BeforeEach |
|
fun setUp() { // <2> |
|
client = MockMvcWebTestClient.bindToApplicationContext(wac).build() // <3> |
|
} |
|
} |
|
---- |
|
<1> Specify the configuration to load |
|
<2> Inject the configuration |
|
<3> Create the `WebTestClient` |
|
|
|
|
|
|
|
[[webtestclient-fn-config]] |
|
=== Bind to Router Function |
|
|
|
This setup allows you to test <<web-reactive.adoc#webflux-fn, functional endpoints>> via |
|
mock request and response objects, without a running server. |
|
|
|
For WebFlux, use the following which delegates to `RouterFunctions.toWebHandler` to |
|
create a server setup to handle requests: |
|
|
|
[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() |
|
---- |
|
|
|
For Spring MVC there are currently no options to test |
|
<<web.adoc#webmvc-fn, WebMvc functional endpoints>>. |
|
|
|
|
|
|
|
[[webtestclient-server-config]] |
|
=== Bind to Server |
|
|
|
This setup connects to a running server to perform full, end-to-end HTTP tests: |
|
|
|
[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 Config |
|
|
|
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 other configuration options, |
|
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()`. See the |
|
<<web-reactive.adoc#webflux-client-body, WebClient>> documentation for examples on how to |
|
prepare a request with any content including form data, multipart data, and more. |
|
|
|
After the call to `exchange()`, `WebTestClient` diverges from the `WebClient` and |
|
instead continues with a workflow to verify responses. |
|
|
|
To assert the response status and headers, use the following: |
|
|
|
[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) |
|
---- |
|
|
|
If you would like for all expectations to be asserted even if one of them fails, you can |
|
use `expectAll(..)` instead of multiple chained `expect*(..)` calls. This feature is |
|
similar to the _soft assertions_ support in AssertJ and the `assertAll()` support in |
|
JUnit Jupiter. |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
.Java |
|
---- |
|
client.get().uri("/persons/1") |
|
.accept(MediaType.APPLICATION_JSON) |
|
.exchange() |
|
.expectAll( |
|
spec -> spec.expectStatus().isOk(), |
|
spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON) |
|
); |
|
---- |
|
|
|
You can then choose to decode the response body through one of the following: |
|
|
|
* `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. |
|
|
|
And perform assertions on the resulting higher level Object(s): |
|
|
|
[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) |
|
---- |
|
|
|
If the built-in assertions are insufficient, you can consume the object instead and |
|
perform any other assertions: |
|
|
|
[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)... |
|
} |
|
---- |
|
|
|
Or you can exit the workflow and obtain an `EntityExchangeResult`: |
|
|
|
[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 is not expected to have content, you can assert that as follows: |
|
|
|
[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() |
|
---- |
|
|
|
If you want to ignore the response content, the following releases the content without |
|
any assertions: |
|
|
|
[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>() |
|
---- |
|
|
|
|
|
|
|
[[webtestclient-json]] |
|
=== JSON Content |
|
|
|
You can use `expectBody()` without a target type to perform assertions on the raw |
|
content rather than through higher level Object(s). |
|
|
|
To verify the full JSON content with https://jsonassert.skyscreamer.org[JSONAssert]: |
|
|
|
[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\"}") |
|
---- |
|
|
|
To verify JSON content with https://github.com/jayway/JsonPath[JSONPath]: |
|
|
|
[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 potentially infinite streams such as `"text/event-stream"` or |
|
`"application/x-ndjson"`, start by verifying the response status and headers, and then |
|
obtain a `FluxExchangeResult`: |
|
|
|
[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're ready to consume the response stream with `StepVerifier` from `reactor-test`: |
|
|
|
[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-mockmvc]] |
|
=== MockMvc Assertions |
|
|
|
`WebTestClient` is an HTTP client and as such it can only verify what is in the client |
|
response including status, headers, and body. |
|
|
|
When testing a Spring MVC application with a MockMvc server setup, you have the extra |
|
choice to perform further assertions on the server response. To do that start by |
|
obtaining an `ExchangeResult` after asserting the body: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
.Java |
|
---- |
|
// For a response with a body |
|
EntityExchangeResult<Person> result = client.get().uri("/persons/1") |
|
.exchange() |
|
.expectStatus().isOk() |
|
.expectBody(Person.class) |
|
.returnResult(); |
|
|
|
// For a response without a body |
|
EntityExchangeResult<Void> result = client.get().uri("/path") |
|
.exchange() |
|
.expectBody().isEmpty(); |
|
---- |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
.Kotlin |
|
---- |
|
// For a response with a body |
|
val result = client.get().uri("/persons/1") |
|
.exchange() |
|
.expectStatus().isOk() |
|
.expectBody(Person.class) |
|
.returnResult(); |
|
|
|
// For a response without a body |
|
val result = client.get().uri("/path") |
|
.exchange() |
|
.expectBody().isEmpty(); |
|
---- |
|
|
|
Then switch to MockMvc server response assertions: |
|
|
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"] |
|
.Java |
|
---- |
|
MockMvcWebTestClient.resultActionsFor(result) |
|
.andExpect(model().attribute("integer", 3)) |
|
.andExpect(model().attribute("string", "a string value")); |
|
---- |
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] |
|
.Kotlin |
|
---- |
|
MockMvcWebTestClient.resultActionsFor(result) |
|
.andExpect(model().attribute("integer", 3)) |
|
.andExpect(model().attribute("string", "a string value")); |
|
---- |
|
|
|
|