12.3.1 Kiểm thử các yêu cầu GET
Một điều chúng ta muốn xác nhận về phương thức recentTacos() là nếu một yêu cầu HTTP GET được gửi đến đường dẫn /api/tacos?recent, thì phản hồi sẽ chứa một payload JSON với không quá 12 taco. Lớp kiểm thử trong liệt kê tiếp theo là một khởi đầu tốt.
Liệt kê 12.1 Sử dụng WebTestClient để kiểm thử TacoController
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();
}
...
}Điều đầu tiên mà phương thức shouldReturnRecentTacos() thực hiện là thiết lập dữ liệu kiểm thử dưới dạng một Flux<Taco>. Flux này sau đó được dùng làm giá trị trả về từ phương thức findAll() của một mock TacoRepository.
Đối với các đối tượng Taco sẽ được phát ra bởi Flux, chúng được tạo ra bằng một phương thức tiện ích có tên là testTaco(), khi được cung cấp một số nguyên, sẽ tạo ra một đối tượng Taco có ID và tên dựa trên số đó. Phương thức testTaco() được triển khai như sau:
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;
}Để đơn giản, tất cả các taco kiểm thử sẽ có cùng hai nguyên liệu. Tuy nhiên, ID và tên của chúng sẽ được xác định bởi số được truyền vào.
Trong khi đó, quay lại với phương thức shouldReturnRecentTacos(), bạn đã khởi tạo một TacoController, truyền mock TacoRepository vào constructor. Controller này được cung cấp cho WebTestClient.bindToController() để tạo một thể hiện của WebTestClient.
Khi đã hoàn tất phần thiết lập, bạn đã sẵn sàng sử dụng WebTestClient để gửi yêu cầu GET tới /api/tacos?recent và kiểm tra xem phản hồi có đúng như mong đợi không. Gọi get().uri("/api/tacos?recent") mô tả yêu cầu bạn muốn gửi. Sau đó, gọi exchange() để gửi yêu cầu, yêu cầu này sẽ được xử lý bởi controller mà WebTestClient đã liên kết — chính là TacoController.
Cuối cùng, bạn có thể xác nhận rằng phản hồi là như mong đợi. Bằng cách gọi expectStatus(), bạn khẳng định rằng phản hồi có mã trạng thái HTTP 200 (OK). Sau đó, bạn sẽ thấy một số lệnh gọi jsonPath() nhằm xác nhận rằng JSON trong phần thân phản hồi chứa các giá trị mong đợi. Kiểm tra cuối cùng xác nhận rằng phần tử thứ 12 (trong một mảng đánh chỉ số từ 0) không tồn tại, vì kết quả không bao giờ được có quá 12 phần tử.
Nếu JSON trả về phức tạp, với nhiều dữ liệu hoặc dữ liệu lồng nhau sâu, thì việc sử dụng jsonPath() có thể gây phiền toái. Trên thực tế, tôi đã lược bớt nhiều lệnh gọi jsonPath() trong liệt kê 12.1 để tiết kiệm không gian. Trong những trường hợp mà jsonPath() trở nên bất tiện, WebTestClient cung cấp phương thức json(), cho phép so sánh phản hồi với một chuỗi JSON.
Ví dụ, giả sử bạn đã tạo JSON phản hồi đầy đủ trong một tệp tên là recent-tacos.json và đặt nó trong classpath dưới đường dẫn /tacos. Khi đó bạn có thể viết lại các xác nhận của WebTestClient như sau:
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);vì json() chấp nhận một String, nên bạn cần tải tài nguyên trong classpath vào một chuỗi. May mắn thay, StreamUtils của Spring giúp bạn dễ dàng với copyToString(). Chuỗi được trả về từ copyToString() sẽ chứa toàn bộ JSON bạn kỳ vọng trong phản hồi của yêu cầu. Đưa chuỗi đó vào phương thức json() sẽ giúp đảm bảo controller đang trả về đúng nội dung.
Một lựa chọn khác do WebTestClient cung cấp cho phép bạn so sánh phần thân phản hồi với một danh sách các giá trị. Phương thức expectBodyList() nhận vào một Class hoặc một ParameterizedTypeReference để chỉ định kiểu phần tử trong danh sách và trả về một đối tượng ListBodySpec để thực hiện xác nhận. Bằng cách sử dụng expectBodyList(), bạn có thể viết lại kiểm thử để sử dụng một tập con của dữ liệu kiểm thử bạn đã dùng để tạo mock TacoRepository, như thể hiện dưới đây:
testClient.get().uri("/api/tacos?recent")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectBodyList(Taco.class)
.contains(Arrays.copyOf(tacos, 12));Tại đây bạn xác nhận rằng phần thân phản hồi chứa một danh sách có cùng các phần tử như 12 phần tử đầu tiên của mảng Taco gốc mà bạn tạo ở đầu phương thức kiểm thử.
