3.6. WebTestClient
预计阅读时间: 6 分钟
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.
Kotlin users:
See this section
related to use of the WebTestClient.
(#webtestclient-setup)3.6.1. 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 WebFlux Java config,
registers the given controller(s), and creates
a WebHandler chain
to handle requests:
WebTestClient client =
WebTestClient.bindToController(new TestController()).build();
val client = WebTestClient.bindToController(TestController()).build()
For Spring MVC, use the following 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:
WebTestClient client =
MockMvcWebTestClient.bindToController(new TestController()).build();
val client = MockMvcWebTestClient.bindToController(TestController()).build()
(#webtestclient-context-config)Bind toApplicationContext
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 WebHttpHandlerBuilder
to create
the WebHandler chain
to handle requests:
@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
@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 MockMvcBuilders.webAppContextSetup
to create a MockMvc instance to handle requests:
@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
@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 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:
RouterFunction<?> route = ...
client = WebTestClient.bindToRouterFunction(route).build();
val route: RouterFunction<*> = ...
val client = WebTestClient.bindToRouterFunction(route).build()
(#webtestclient-server-config)Bind to Server
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()
(#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:
client = WebTestClient.bindToController(new TestController())
.configureClient()
.baseUrl("/test")
.build();
client = WebTestClient.bindToController(TestController())
.configureClient()
.baseUrl("/test")
.build()
(#webtestclient-tests)3.6.2. Writing Tests
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 following:
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)
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.
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 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()
When you need to decode to a target type with generics, look for the overloaded methods that accept
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:
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>()
(#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 JSONAssert:
为了验证完整的 JSON 内容使用 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:
为了验证 JSON 内容与 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")
(#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:
测试可能无限的数据流,例如 "text/event-stream" 或 "application/x-ndjson" ,首先验证响应状态和头部信息,然后获取一个
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:
现在您已准备好使用 reactor-test 中的 StepVerifier 消费响应流:
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-mockmvc)MockMvc Assertions 模拟 Mvc 断言
WebTestClient is an HTTP client and as such it can only verify what is in the client response including status,
headers, and body.
WebTestClient 是一个 HTTP 客户端,因此它只能验证客户端响应中的内容,包括状态、头信息和主体。
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:
在测试使用 MockMvc 服务器设置的 Spring MVC 应用程序时,您可以选择进一步对服务器响应进行断言。为此,首先在断言正文后获取一个
ExchangeResult 。
// 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:
然后切换到 MockMvc 服务器响应断言:
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"));