Part 02 · Testing & Code Quality · 2.1.02

JUnit Platform, Jupiter và execution lifecycle

JUnit hiện đại là kiến trúc launcher–engine, không chỉ là một bộ annotation để viết test. Hiểu các lớp này giúp phân biệt lỗi compile, discovery, execution và build integration.


1. Kiến trúc Platform, Engine và Jupiter

JUnit Platform là nền tảng launch test engines, cung cấp discovery, Launcher API và integration với IDE/build tool. TestEngine chịu trách nhiệm tạo test plan, cây descriptor và thực thi node. JUnit Jupiter cung cấp programming model và extension model hiện đại để viết/chạy test.

Vì vậy, compile thành công không bảo đảm test đã được discover. Một project có thể có Jupiter API để biên dịch nhưng thiếu Jupiter engine, cấu hình plugin sai hoặc filter khiến CI không chạy test nào.

Senior signal: khi điều tra “IDE chạy nhưng CI bỏ qua”, kiểm tra engine dependency, test naming, tag/filter, module path và version của build plugin trước khi kết luận test code bị lỗi.

2. Discovery → execution → report

  1. IDE hoặc build plugin tạo request với selector, filter và configuration parameters.
  2. Platform hỏi từng TestEngine để discover test plan.
  3. Engine tạo container/test nodes, chạy lifecycle callback và phát execution events.
  4. Launcher/build tool thu report, xử lý failure và quyết định exit status.

Pipeline có thể “xanh giả” nếu test bị filter, disable hoặc không được discover nhưng build không có fail-fast check cho số lượng test đã chạy. CI nên lưu report và có policy phát hiện test count bất thường.

3. Test instance và fixture lifecycle

Mặc định JUnit tạo một test instance mới cho mỗi test method (PER_METHOD). Đây là lựa chọn an toàn hơn vì giảm shared mutable state và order dependency. PER_CLASS dùng lại instance giữa các method, cho phép non-static lifecycle methods nhưng làm state dễ leak giữa test và tăng rủi ro khi bật parallel execution.

Với fixture dùng chung, phải xác định ownership, cleanup và thread-safety. Database record, system property, file tạm, port và external resource không được tồn tại ngầm sau test.

@ParameterizedTest
@CsvSource({"0,false", "1,true", "10,true"})
void validatesQuantity(int quantity, boolean expected) {
    assertEquals(expected, OrderRules.isValidQuantity(quantity));
}

4. Parameters, extensions và parallelism

Parameterized tests

@ParameterizedTest giúp diễn đạt nhiều input/output case mà không nhân bản cùng một test method. Input nên đại diện cho boundary và failure case có ý nghĩa; đừng biến một test thành bảng dữ liệu khổng lồ không còn dễ đọc.

Extensions

Extension có thể tham gia condition evaluation, parameter resolution, lifecycle callback và exception handling. Chỉ tạo extension cho concern tái sử dụng xuyên suite; nếu logic chỉ phục vụ một test, fixture bình thường thường dễ đọc và dễ debug hơn.

Parallel execution

Parallel test cần isolation hoặc resource lock cho system properties, files, ports, database state và mock/thread safety. Chuyển sang parallel không tự động làm pipeline nhanh hơn nếu fixture contention và container startup trở thành bottleneck.

5. Failure modes và cách chẩn đoán

6. Câu hỏi phỏng vấn đào sâu

7. Checklist tự đánh giá

Nguồn tham khảo