11.3 测试响应式接口
11.3 测试响应式 Controller
在测试响应式 Controller 时,Spring 5 并没有让我们陷入困境。实际上,Spring 5 引入了 WebTestClient,这是一个新的测试程序,它让用 Spring WebFlux 编写的响应式 Controller 变得容易测试。让我们首先使用它测试在第 11.1.2 节中编写的 DesignTacoController 中的 recentTacos() 方法,来了解如何使用 WebTestClient 编写测试用例。
11.3.1 测试 GET 请求
对于 recentTacos() 方法,我们想声明的一件事是,如果为 /design/recent
路径发出了 HTTP GET 请求,那么响应将包含一个不超过 12 个 tacos 的 JSON 数据。程序清单 11.1 中的测试类是一个很好的开始。
程序清单 11.1 使用 WebTestClient 测试 DesignTacoController
package tacos;
import static org.mockito.Mockito.*;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;
import tacos.Ingredient.Type;
import tacos.data.TacoRepository;
import tacos.web.api.DesignTacoController;
public class DesignTacoControllerTest {
@Test
public void shouldReturnRecentTacos() {
Taco[] tacos = {
testTaco(1L), testTaco(2L), testTaco(3L), testTaco(4L),
testTaco(5L), testTaco(6L), testTaco(7L), testTaco(8L),
testTaco(9L), testTaco(10L), testTaco(11L), testTaco(12L),
testTaco(13L), testTaco(14L), testTaco(15L), testTaco(16L)
};
Flux<Taco> tacoFlux = Flux.just(tacos);
TacoRepository tacoRepo = Mockito.mock(TacoRepository.class);
when(tacoRepo.findAll()).thenReturn(tacoFlux);
WebTestClient testClient = WebTestClient.bindToController(
new DesignTacoController(tacoRepo)).build();
testClient.get().uri("/design/recent")
.exchange().expectStatus().isOk().expectBody()
.jsonPath("$").isArray()
.jsonPath("$").isNotEmpty()
.jsonPath("$[0].id").isEqualTo(tacos[0].getId().toString())
.jsonPath("$[0].name").isEqualTo("Taco 1")
.jsonPath("$[1].id").isEqualTo(tacos[1].getId().toString())
.jsonPath("$[1].name").isEqualTo("Taco 2")
.jsonPath("$[11].id").isEqualTo(tacos[11].getId().toString())
...
.jsonPath("$[11].name").isEqualTo("Taco 12")
.jsonPath("$[12]").doesNotExist()
.jsonPath("$[12]").doesNotExist();
}
...
}
shouldReturnRecentTacos() 方法做的第一件事是以 Flux
对于将由 Flux 发布的 Taco 对象,它们是用一个名为 testTaco() 的方法创建的,当给定一个数字时,该方法将生成一个 Taco 对象,其 ID 和名称基于该数字。testTaco() 方法实现如下:
private Taco testTaco(Long number) {
Taco taco = new Taco();
taco.setId(UUID.randomUUID());
taco.setName("Taco " + number);
List<IngredientUDT> ingredients = new ArrayList<>();
ingredients.add(
new IngredientUDT("INGA", "Ingredient A", Type.WRAP));
ingredients.add(
new IngredientUDT("INGB", "Ingredient B", Type.PROTEIN));
taco.setIngredients(ingredients);
return taco;
}
为了简单起见,所有的测试 tacos 都有相同的两种成分。但它们的 ID 和名字将由给定的号码决定。
同时,在 shouldReturnRecentTacos() 方法中,实例化了一个 DesignTacoController,将模拟的 TacoRepository 注入构造函数。Controller 被赋予 WebTestClient.bindToController() 以创建 WebTestClient 的实例。
完成所有设置后,现在可以使用 WebTestClient 向 /design/recent
提交 GET 请求,并验证响应是否满足预期。调用 get().uri(“/design/recent”) 描述要发出的请求。然后调用 exchange() 提交请求,该请求将由绑定到 DesignTacoController 的 Controller 进行处理。
最后,可以确认响应与预期一致。通过调用 expectStatus(),可以断言响应具有 HTTP 200(OK) 状态代码。之后,将看到对 jsonPath() 的几个调用,这些调用断言响应体中的 JSON 具有它应该具有的值。最后的断言检查第 12 个元素(在基于零的数组中)是否不存在,因为结果不应超过 12 个元素。
如果 JSON 返回很复杂,包含大量数据或高度嵌套的数据,那么使用 jsonPath() 可能会很无聊。实际上,为了节省空间,清单 11.1 中已经省略了对 jsonPath() 的许多调用。对于那些使用 jsonPath() 可能很笨拙的情况,WebTestClient 提供了 json(),它接受包含 json 的 String 参数来对响应进行响应。
例如,假设在一个名为 recent-tacos.JSON 的文件中创建了完整的响应 JSON,并将其放在路径 /tacos
下的类路径中。然后重写 WebTestClient 断言,如下所示:
ClassPathResource recentsResource = new ClassPathResource("/tacos/recent-tacos.json");
String recentsJson = StreamUtils.copyToString(
recentsResource.getInputStream(), Charset.defaultCharset());
testClient.get().uri("/design/recent")
.accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isOk().expectBody()
.json(recentsJson);
因为 json() 接受 String,所以必须首先将类路径资源加载到 String 对象中。谢天谢地,Spring 中的 StreamUtils 使 copyToString() 的使用变得简单。copyToString() 返回的 String 将包含响应请求时预期的整个 JSON。将它赋给 json() 方法可以确保 Controller 产生正确的输出。
WebTestClient 提供的另一个选项允许将响应体与值列表进行比较。expectBodyList() 方法接受指示列表中元素类型的类或参数化类型引用,并返回要针对其进行断言的 istBodySpec 对象。使用 expectBodyList(),可以重写测试以使用用于创建模拟 TacoRepository 的相同测试数据的子集:
testClient.get().uri("/design/recent")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectBodyList(Taco.class)
.contains(Arrays.copyOf(tacos, 12));
在这里,断言响应体包含的列表,与在测试方法开始时创建的原始 Taco 数组的前 12 个元素,具有相同的元素。
11.3.2 测试 POST 请求
WebTestClient 可以做的不仅仅是针对 Controller 的 GET 请求进行测试。它还可以用于测试任何类型的 HTTP 方法,包括 GET、POST、PUT、PATCH、DELETE 和 HEAD 请求。表 11.1 将 HTTP 方法映射到 WebTestClient 方法。
表 11.1 WebTestClient 针对 Spring WebFlux 控制器测试任何类型的请求
HTTP 方法 | WebTestClient 方法 |
---|---|
GET | .get() |
POST | .post() |
PUT | .put() |
PATCH | .patch() |
DELETE | .delete() |
HEAD | .head() |
作为针对 Spring WebFlux Controller 中的另一个 HTTP 方法请求示例的测试,让我们看看针对 DesignTacoController 的另一个测试。这次,将通过向 /design
提交 POST 请求来编写针对创建 taco 端点 API 的测试:
@Test
public void shouldSaveATaco() {
TacoRepository tacoRepo = Mockito.mock(TacoRepository.class);
Mono<Taco> unsavedTacoMono = Mono.just(testTaco(null));
Taco savedTaco = testTaco(null);
savedTaco.setId(1L);
Mono<Taco> savedTacoMono = Mono.just(savedTaco);
when(tacoRepo.save(any())).thenReturn(savedTacoMono);
WebTestClient testClient = WebTestClient.bindToController(
new DesignTacoController(tacoRepo)).build();
testClient.post()
.uri("/design")
.contentType(MediaType.APPLICATION_JSON)
.body(unsavedTacoMono, Taco.class)
.exchange()
.expectStatus().isCreated()
.expectBody(Taco.class)
.isEqualTo(savedTaco);
}
与前面的测试方法一样,shouldSaveATaco() 首先设置一些测试数据,模拟 TacoRepository,然后构建一个 WebTestClient,并绑定到 Controller。然后,使用 WebTestClient 向 /design
提交 POST 请求,请求的 body 类型为 application/json,有效负载是未保存 Mono 中 Taco 的 json 序列化形式。在执行 exchange() 之后,测试断言响应具有 HTTP 201(CREATED) 状态,并且正文中的有效负载等于保存的 Taco 对象。
11.3.3 使用线上服务器进行测试
到目前为止,编写的测试依赖于 Spring WebFlux 框架的模拟实现,因此不需要真正的服务器。但可能需要在 Netty 或 Tomcat 等服务器的上下文中测试 WebFlux Controller,并且可能需要使用 repository 或其他依赖项。也就是说,可能需要编写一个集成测试。
要编写 WebTestClient 集成测试,首先使用 @RunWith 和 @SpringBootTest 对测试类进行注解,就像其他任何 Spring Boot 集成测试一样:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
public class DesignTacoControllerWebTest {
@Autowired
private WebTestClient testClient;
}
通过将 webEnvironment 属性设置为 webEnvironment.RANDOM_PORT,将要求 Spring 启动正在运行的服务器来监听随机选择的端口。
WebTestClient 还将自动连接到测试类中。这不仅意味着将不再需要在测试方法中创建一个 URL,而且在发出请求时也不需要指定完整的 URL。这是因为 WebTestClient 将被装配成知道测试服务器在哪个端口上运行。现在可以将 shouldReturnRecentTacos() 重写为使用自动连线 WebTestClient 的集成测试:
@Test
public void shouldReturnRecentTacos() throws IOException {
testClient.get().uri("/design/recent")
.accept(MediaType.APPLICATION_JSON).exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[?(@.id == 'TACO1')].name").isEqualTo("Carnivore")
.jsonPath("$[?(@.id == 'TACO2')].name").isEqualTo("Bovine Bounty")
.jsonPath("$[?(@.id == 'TACO3')].name").isEqualTo("Veg-Out");
}
毫无疑问,你已经注意到这个新版本的 shouldReturnRecentTacos() 的代码要少得多。因为将使用自动注入的实例,因此不再需要创建 WebTestClient。而且没有必要模拟 TacoRepository,因为 Spring 将创建 DesignTacoController 的一个实例,并为它注入一个真正的 TacoRepository。在这个新版本的测试方法中,使用 JSONPath 表达式来验证从数据库提供的值。
在测试过程中,当需要使用 WebFlux Controller 公开的 API 时,WebTestClient 非常有用。但是,当应用程序本身使用其他 API 时呢?让我们把注意力转向 Spring 的响应式 web 的客户端,看看 WebClient 是如何提供 REST 客户端来处理诸如 Mono 和 Flux 之类的响应式类型的。