12.3.1 测试 GET 请求
对于 recentTacos()
方法,我们想声明的一件事是,如果为 /api/tacos?recent
路径发出了 HTTP GET 请求,那么响应将包含一个不超过 12 个 tacos 的 JSON 数据。下面清单中的测试类是一个很好的开始。
程序清单 12.1 使用 WebTestClient 测试 DesignTacoController
package tacos.web.api;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import tacos.Ingredient;
import tacos.Ingredient.Type;
import tacos.Taco;
import tacos.data.TacoRepository;
public class TacoControllerTest {
@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 TacoController(tacoRepo))
.build();
testClient.get().uri("/api/tacos?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();
}
...
}
shouldReturnRecentTacos()
方法做的第一件事是以 Flux<Taco>
的形式设置测试数据。然后,这个 Flux 作为模拟 TacoRepository 的 findAll()
方法的返回值。
对于将由 Flux 发布的 Taco 对象,它们是用一个名为 testTaco()
的方法创建的,当给定一个数字时,该方法将生成一个 Taco 对象,其 ID 和名称基于该数字。testTaco()
方法实现如下:
private Taco testTaco(Long number) {
Taco taco = new Taco();
taco.setId(number != null ? number.toString(): "TESTID");
taco.setName("Taco " + number);
List<Ingredient> ingredients = new ArrayList<>();
ingredients.add(
new Ingredient("INGA", "Ingredient A", Type.WRAP));
ingredients.add(
new Ingredient("INGB", "Ingredient B", Type.PROTEIN));
taco.setIngredients(ingredients);
return taco;
}
为了简单起见,所有的测试 tacos 都有相同的两种成分。但它们的 ID 和名字将由给定的号码决定。
同时,在 shouldReturnRecentTacos()
方法中,实例化了一个 TacoController,将模拟的 TacoRepository 注入构造函数。Controller 被赋予 WebTestClient.bindToController() 以创建 WebTestClient 的实例。
完成所有设置后,现在可以使用 WebTestClient 向 /api/tacos?recent
提交 GET 请求,并验证响应是否满足预期。调用 get().uri(“/api/tacos?recent”) 描述要发出的请求。然后调用 exchange()
提交请求,该请求将由绑定到 TacoController 的 Controller 进行处理。
最后,可以确认响应与预期一致。通过调用 expectStatus()
,可以断言响应具有 HTTP 200(OK) 状态代码。之后,将看到对 jsonPath()
的几个调用,这些调用断言响应体中的 JSON 具有它应该具有的值。最后的断言检查第 12 个元素(在基于零的数组中)是否不存在,因为结果不应超过 12 个元素。
如果 JSON 返回很复杂,包含大量数据或高度嵌套的数据,那么使用 jsonPath()
可能会很无聊。实际上,为了节省空间,清单 12.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("/api/tacos?recent")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectBody()
.json(recentsJson);
因为 json()
接受 String,所以必须首先将类路径资源加载到 String 对象中。谢天谢地,Spring 中的 StreamUtils 使 copyToString()
的使用变得简单。copyToString()
返回的 String 将包含响应请求时预期的整个 JSON。将它赋给 json()
方法可以确保 Controller 产生正确的输出。
WebTestClient 提供的另一个选项,允许将响应体与值列表进行比较。expectBodyList()
方法接受指示列表中元素类型的 Class
或 ParameterizedTypeReference
,并返回要针对其进行断言的 istBodySpec 对象。使用 expectBodyList()
,可以重写测试以使用用于创建模拟 TacoRepository 的相同测试数据的子集:
testClient.get().uri("/api/tacos?recent")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectBodyList(Taco.class)
.contains(Arrays.copyOf(tacos, 12));
在这里,断言响应体包含的列表,与在测试方法开始时创建的原始 Taco 数组的前 12 个元素,具有相同的元素。