WebTestClient
is an HTTP client designed for testing server applications. It wraps
Spring’s 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 this section
related to use of the WebTestClient .
|
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.
This setup allows you to test specific controller(s) via mock request and response objects, without a running server.
For WebFlux applications, use the below which loads infrastructure equivalent to the WebFlux Java config, registers the given controller(s), and creates a WebHandler chain to handle requests with:
WebTestClient client =
WebTestClient.bindToController(new TestController()).build();
val client = WebTestClient.bindToController(TestController()).build()
For Spring MVC, use the below which delegates to the StandaloneMockMvcBuilder to load infrastructure equivalent to the WebMvc Java config, registers the given controller(s), and creates an instance of MockMvc to handle requests with:
WebTestClient client =
MockMvcWebTestClient.bindToController(new TestController()).build();
val client = MockMvcWebTestClient.bindToController(TestController()).build()
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 below where the Spring ApplicationContext
is passed to
WebHttpHandlerBuilder
to create the WebHandler chain to handle
requests with:
@SpringJUnitConfig(WebConfig.class) // (1)
class MyTests {
WebTestClient client;
@BeforeEach
void setUp(ApplicationContext context) { // (2)
client = WebTestClient.bindToApplicationContext(context).build(); // (3)
}
}
-
Specify the configuration to load
-
Inject the configuration
-
Create the
WebTestClient
@SpringJUnitConfig(WebConfig::class) // (1)
class MyTests {
lateinit var client: WebTestClient
@BeforeEach
fun setUp(context: ApplicationContext) { // (2)
client = WebTestClient.bindToApplicationContext(context).build() // (3)
}
}
-
Specify the configuration to load
-
Inject the configuration
-
Create the
WebTestClient
For Spring MVC, use the below where the Spring ApplicationContext
is passed to
MockMvcBuilders.html#webAppContextSetup
to create a MockMvc instance to handle
requests with:
@ExtendWith(SpringExtension.class)
@WebAppConfiguration("classpath:META-INF/web-resources") // (1)
@ContextHierarchy({
@ContextConfiguration(classes = RootConfig.class),
@ContextConfiguration(classes = WebConfig.class)
})
class MyTests {
@Autowired
private WebApplicationContext wac; // (2)
WebTestClient client;
@BeforeEach
void setUp() {
client = MockMvcWebTestClient.bindToApplicationContext(this.wac).build(); // (3)
}
}
-
Specify the configuration to load
-
Inject the configuration
-
Create the
WebTestClient
@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 = WebTestClient.bindToApplicationContext(context).build() // (3)
}
}
-
Specify the configuration to load
-
Inject the configuration
-
Create the
WebTestClient
This setup allows you to test functional endpoints via mock request and response objects, without a running server.
For WebFlux, use the below which delegates to RouterFunctions.toWebHandler
to create a
server setup to handle requests with:
RouterFunction<?> route = ...
client = WebTestClient.bindToRouterFunction(route).build();
val route: RouterFunction<*> = ...
val client = WebTestClient.bindToRouterFunction(route).build()
For Spring MVC there is currently no options to test WebMvc functional endpoints.
This setup connects to a running server to perform full, end-to-end HTTP tests:
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build()
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:
client = WebTestClient.bindToController(new TestController())
.configureClient()
.baseUrl("/test")
.build();
client = WebTestClient.bindToController(TestController())
.configureClient()
.baseUrl("/test")
.build()
WebTestClient
provides an API identical to WebClient
up to the point of performing a request by using exchange()
. See the
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 below:
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.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 toList<T>
. -
expectBody()
: Decode tobyte[]
for JSON Content or an empty body.
And perform assertions on the resulting higher level Object(s):
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBodyList(Person.class).hasSize(3).contains(person);
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:
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)...
});
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
:
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();
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
ParameterizedTypeReference
instead of Class<T> .
|
If the response is not expected to have content, you can assert that as follows:
client.post().uri("/persons")
.body(personMono, Person.class)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty();
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:
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound()
.expectBody(Void.class);
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound
.expectBody<Unit>()
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 JSONAssert:
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")
To verify JSON content with JSONPath:
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason");
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason")
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
:
FluxExchangeResult<MyEvent> result = client.get().uri("/events")
.accept(TEXT_EVENT_STREAM)
.exchange()
.expectStatus().isOk()
.returnResult(MyEvent.class);
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
:
Flux<Event> eventFlux = result.getResponseBody();
StepVerifier.create(eventFlux)
.expectNext(person)
.expectNextCount(4)
.consumeNextWith(p -> ...)
.thenCancel()
.verify();
val eventFlux = result.getResponseBody()
StepVerifier.create(eventFlux)
.expectNext(person)
.expectNextCount(4)
.consumeNextWith { p -> ... }
.thenCancel()
.verify()
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:
// 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();
// 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:
MockMvcWebTestClient.resultActionsFor(result)
.andExpect(model().attribute("integer", 3))
.andExpect(model().attribute("string", "a string value"));
MockMvcWebTestClient.resultActionsFor(result)
.andExpect(model().attribute("integer", 3))
.andExpect(model().attribute("string", "a string value"));