WebTestClient

WebTestClient 是一款旨在测试服务器应用的 HTTP 客户端。它包装 Spring 的 WebClient 并使用它执行请求,但公开了一个用于验证响应的测试外观。WebTestClient 可用于执行端到端 HTTP 测试。它还可用于通过模拟服务器请求和响应对象在没有运行的服务器的情况下测试 Spring MVC 和 Spring WebFlux 应用。

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.

Setup

要设置 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.

Bind to Controller

此设置允许您通过模拟请求和响应对象在没有运行的服务器的情况下测试特定的控制器。

This setup allows you to test specific controller(s) via mock request and response objects, without a running server.

对于 WebFlux 应用程序,请使用以下内容,它将加载与 WebFlux Java config 等效的基础设施,注册给定的控制器,然后创建一个 WebHandler chain 来处理请求:

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:

  • Java

  • Kotlin

WebTestClient client =
		WebTestClient.bindToController(new TestController()).build();
val client = WebTestClient.bindToController(TestController()).build()

对于 Spring MVC,请使用以下代码来委派 StandaloneMockMvcBuilder 来加载与 WebMvc Java config 等效的基础设施、注册给定的控制器,并创建一个 MockMvc 实例来处理请求:

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:

  • Java

  • Kotlin

WebTestClient client =
		MockMvcWebTestClient.bindToController(new TestController()).build();
val client = MockMvcWebTestClient.bindToController(TestController()).build()

Bind to ApplicationContext

此设置允许您使用 Spring MVC 或 Spring WebFlux 基础设施和控制器声明加载 Spring 配置,并使用它通过模拟请求和响应对象来处理请求,而无需运行的服务器。

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.

对于 WebFlux,请使用 Spring ApplicationContext 传递给 WebHttpHandlerBuilder 的内容,以创建 WebHandler chain 来处理请求:

For WebFlux, use the following where the Spring ApplicationContext is passed to WebHttpHandlerBuilder to create the WebHandler chain to handle requests:

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
Kotlin
@SpringJUnitConfig(WebConfig::class) (1)
class MyTests {

	lateinit var client: WebTestClient

	@BeforeEach
	fun setUp(context: ApplicationContext) { (2)
		client = WebTestClient.bindToApplicationContext(context).build() (3)
	}
}
4 Specify the configuration to load
5 Inject the configuration
6 Create the WebTestClient

对于 Spring MVC,请使用以下代码,其中将 Spring ApplicationContext 传递给 MockMvcBuilders.webAppContextSetup 以创建一个 MockMvc 实例来处理请求:

For Spring MVC, use the following where the Spring ApplicationContext is passed to MockMvcBuilders.webAppContextSetup to create a MockMvc instance to handle requests:

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
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)
	}
}
4 Specify the configuration to load
5 Inject the configuration
6 Create the WebTestClient

Bind to Router Function

此设置允许您通过模拟请求和响应对象来测试 functional endpoints,而无需运行服务器。

This setup allows you to test functional endpoints via mock request and response objects, without a running server.

对于 WebFlux,请使用委托给 RouterFunctions.toWebHandler 以创建服务器设置来处理请求的以下内容:

For WebFlux, use the following which delegates to RouterFunctions.toWebHandler to create a server setup to handle requests:

  • Java

  • Kotlin

RouterFunction<?> route = ...
client = WebTestClient.bindToRouterFunction(route).build();
val route: RouterFunction<*> = ...
val client = WebTestClient.bindToRouterFunction(route).build()

对于 Spring MVC,目前没有测试 WebMvc functional endpoints 的选项。

For Spring MVC there are currently no options to test WebMvc functional endpoints.

Bind to Server

该设置连接到正在运行的服务器以执行完全的端到端的 HTTP 测试:

This setup connects to a running server to perform full, end-to-end HTTP tests:

  • Java

  • Kotlin

client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build()

Client Config

除了前面描述的服务器设置选项之外,您还可以配置客户端选项,包括基本 URL、默认标题、客户端过滤器等。这些选项在 bindToServer() 之后很容易用。对于所有其他配置选项,您需要使用 configureClient() 从服务器配置转换到客户端配置,如下所示:

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:

  • Java

  • Kotlin

client = WebTestClient.bindToController(new TestController())
		.configureClient()
		.baseUrl("/test")
		.build();
client = WebTestClient.bindToController(TestController())
		.configureClient()
		.baseUrl("/test")
		.build()

Writing Tests

WebTestClient 提供了一个与 WebClient 等效的 API,直到使用 exchange() 执行请求。有关如何使用任何内容(包括表单数据、多部分数据等)准备请求的示例,请参见 WebClient 文档。

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.

在调用 exchange() 之后,WebTestClient 偏离 WebClient,而是继续使用工作流来验证响应。

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:

  • Java

  • Kotlin

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)

如果您希望断言所有期望,即使其中一个失败,您也可以使用 expectAll(..) 代替多个链式 expect*(..) 调用。此功能类似于 AssertJ 中的 软断言 支持和 JUnit Jupiter 中的 assertAll() 支持。

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.

  • 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 JSON Content or an empty body.

并对结果的高级对象执行断言:

And perform assertions on the resulting higher level Object(s):

  • Java

  • Kotlin

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:

  • Java

  • Kotlin

   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)...
		}

或者,您可以退出工作流并获取 EntityExchangeResult

Or you can exit the workflow and obtain an EntityExchangeResult:

  • Java

  • Kotlin

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()

当你需要解码到具有泛型的目标类型时,请寻找接受 ParameterizedTypeReference(而不是 Class<T>)的重载方法。

When you need to decode to a target type with generics, look for the overloaded methods that accept ParameterizedTypeReference instead of Class<T>.

No Content

如果响应预计没有内容,您可以如下断言:

If the response is not expected to have content, you can assert that as follows:

  • Java

  • Kotlin

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:

  • Java

  • Kotlin

client.get().uri("/persons/123")
		.exchange()
		.expectStatus().isNotFound()
		.expectBody(Void.class);
client.get().uri("/persons/123")
		.exchange()
		.expectStatus().isNotFound
		.expectBody<Unit>()

JSON Content

您可以使用 expectBody() 而没有目标类型来对原始内容执行断言,而不是通过高级对象。

You can use expectBody() without a target type to perform assertions on the raw content rather than through higher level Object(s).

如需使用 JSONAssert 验证完整 JSON 内容:

To verify the full JSON content with JSONAssert:

  • Java

  • Kotlin

client.get().uri("/persons/1")
		.exchange()
		.expectStatus().isOk()
		.expectBody()
		.json("{\"name\":\"Jane\"}")
client.get().uri("/persons/1")
		.exchange()
		.expectStatus().isOk()
		.expectBody()
		.json("{\"name\":\"Jane\"}")

如需使用 JSONPath 验证 JSON 内容:

To verify JSON content with JSONPath:

  • Java

  • Kotlin

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")

Streaming Responses

要测试无限流(例如 "text/event-stream""application/x-ndjson"),请首先验证响应状态和标题,然后获取 FluxExchangeResult

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:

  • Java

  • Kotlin

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>()

现在,您已准备好使用 reactor-test 中的 StepVerifier 来使用响应流:

Now you’re ready to consume the response stream with StepVerifier from reactor-test:

  • Java

  • Kotlin

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()

MockMvc Assertions

WebTestClient 是一个 HTTP 客户端,因此它只能验证客户端响应,其中包括状态、标头和正文。

WebTestClient is an HTTP client and as such it can only verify what is in the client response including status, headers, and body.

在使用 MockMvc 服务器设置测试 Spring MVC 应用程序时,您可以选择对服务器响应执行进一步断言。为此,在断言正文后开始获取 ExchangeResult

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:

  • Java

  • Kotlin

// 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>()
		.returnResult()

// For a response without a body
val result = client.get().uri("/path")
		.exchange()
		.expectBody().isEmpty()

然后切换到 MockMvc 服务器响应断言:

Then switch to MockMvc server response assertions:

  • Java

  • Kotlin

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"));