99 Ngày Java — Ngày 88: Unit Test với JUnit 5
Unit test là đoạn code kiểm tra một hành vi nhỏ, chạy trong vài mili giây và không cần mạng hay database. JUnit 5, còn gọi là JUnit Jupiter, là framework chuẩn của hệ sinh thái Java. Bài này đi qua phần dùng hằng ngày: vòng đời, assertion và test tham số hóa.

1. @Test và cấu trúc ba bước
Một phương thức có @Test là một ca kiểm thử; lớp và phương thức không cần public. Viết theo ba bước: chuẩn bị dữ liệu, thực hiện đúng một hành động, kiểm tra kết quả. Tên test mô tả hành vi mong đợi, không mô tả cài đặt; @DisplayName cho phép viết tên dạng câu. Mỗi test chỉ nên có một lý do để thất bại.
class PriceCalculatorTest {
private final PriceCalculator calculator = new PriceCalculator();
@Test
@DisplayName("applies a 10% discount to orders above one million")
void appliesDiscountAboveThreshold() {
var order = new Order(1_500_000); // arrange
long total = calculator.total(order); // act
assertEquals(1_350_000, total); // assert
}
}2. Vòng đời: mỗi test một instance
Mặc định JUnit tạo một instance mới của lớp test cho mỗi phương thức, nên trường của lớp không rò rỉ trạng thái giữa các test. @BeforeEach và @AfterEach chạy quanh từng test; @BeforeAll và @AfterAll chạy một lần cho cả lớp và phải là static. Thứ tự chạy các test không được bảo đảm, vì vậy test không được phụ thuộc lẫn nhau. @Nested gom các test cùng ngữ cảnh thành nhóm dễ đọc.
class CartTest {
private Cart cart;
@BeforeEach
void setUp() { cart = new Cart(); } // fresh state for every test
@Nested
class WhenEmpty {
@Test
void totalIsZero() { assertEquals(0, cart.total()); }
}
}3. Assertion đáng nhớ
assertEquals nhận giá trị mong đợi trước, giá trị thực sau; đảo thứ tự làm thông báo lỗi gây hiểu nhầm. assertThrows kiểm tra ngoại lệ và trả về nó để kiểm tra tiếp thông điệp. assertAll chạy mọi kiểm tra bên trong rồi báo tất cả lỗi một lượt, hợp khi kiểm tra nhiều thuộc tính của một kết quả. Thông điệp lỗi nên truyền dạng lambda để chỉ được tạo khi test thất bại.
var error = assertThrows(IllegalArgumentException.class,
() -> new Order(-1));
assertEquals("amount must be positive", error.getMessage());
assertAll("receipt",
() -> assertEquals("PAID", receipt.status()),
() -> assertEquals(1_350_000, receipt.total()),
() -> assertNotNull(receipt.paidAt()));4. Parameterized test
Khi cùng một logic cần thử với nhiều bộ dữ liệu, @ParameterizedTest chạy một phương thức nhiều lần với tham số khác nhau, và mỗi bộ dữ liệu được báo cáo như một test riêng. @ValueSource cho một tham số đơn, @CsvSource cho vài cột, @MethodSource cho dữ liệu phức tạp. Cần thêm artifact junit-jupiter-params, đã nằm sẵn trong junit-jupiter.
Thử giá trị biên: 0, âm, đúng ngưỡng, ngay trên ngưỡng.
Đừng đưa logic tính kết quả mong đợi vào test; ghi thẳng giá trị.
Test chậm hoặc cần hạ tầng thì tách sang integration test.
@ParameterizedTest(name = "{0} -> {1}")
@CsvSource({
"999999, 999999",
"1000000, 1000000",
"1000100, 900090",
})
void discountStartsAboveOneMillion(long amount, long expected) {
assertEquals(expected, calculator.total(new Order(amount)));
}Test tốt đọc như đặc tả: tên nói hành vi, thân test có ba bước rõ ràng. Ngày 89 xử lý phần còn thiếu: cô lập lớp cần test khỏi phụ thuộc của nó bằng Mockito.
Tài liệu đối chiếu
https://docs.junit.org/current/user-guide/
https://docs.junit.org/current/api/org.junit.jupiter.api/org/junit/jupiter/api/Assertions.html
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


