Observable behavior, assertion và fixture
Một test tốt kể rõ điều kiện, hành vi và outcome. Fixture chỉ phục vụ câu chuyện đó, không được che mất behavior đang cần chứng minh.
1. Mental model: test public contract
Test nên quan sát contract thông qua return value, state transition, event hoặc interaction có ý nghĩa về mặt kiến trúc. Không gọi private method trực tiếp để “ép coverage”; nếu logic khó quan sát, hãy tách object hoặc boundary có trách nhiệm rõ hơn.
Arrange–Act–Assert và Given–When–Then là cấu trúc giúp người đọc nhận ra setup, hành động và outcome. Chúng không phải nghi thức cứng: điều quan trọng là mỗi test có một câu chuyện ngắn, deterministic và dễ chẩn đoán.
2. Assertion design
Đừng chỉ assert “không throw”. Hãy assert outcome và invariant liên quan. Một behavior có thể cần nhiều assertion độc lập; assertAll giúp báo nhiều sai lệch cùng lúc nhưng không nên dùng để che một test đang kiểm tra quá nhiều behavior.
Với exception, kiểm tra exception type và dữ liệu nghiệp vụ; đồng thời xác nhận state không bị mutate sai:
var error = assertThrows(
InsufficientBalanceException.class,
() -> account.debit(new BigDecimal("20.00")));
assertAll(
() -> assertEquals("INSUFFICIENT_BALANCE", error.code()),
() -> assertEquals(new BigDecimal("10.00"), account.balance()));
- Ưu tiên assertion có message/failure output giúp định vị nguyên nhân.
- So sánh money với scale/rounding policy nhất quán, không dùng string formatting làm contract ngầm.
- Không dùng assertion quá rộng như snapshot khổng lồ nếu một field cụ thể mới là risk.
3. Fixture nhỏ, rõ intent và có ownership
Fixture nên nhỏ, immutable khi có thể và thể hiện được dữ liệu quan trọng của test. Test Data Builder thường phù hợp hơn Object Mother khổng lồ: builder cung cấp default hợp lệ rồi cho phép override trường chính trong từng scenario.
| Cách tạo fixture | Ưu điểm | Rủi ro |
|---|---|---|
| Inline setup | Intent nằm ngay cạnh assertion, dễ đọc với case nhỏ. | Lặp lại nếu object có nhiều field. |
| Test Data Builder | Default hợp lệ, override rõ field quan trọng. | Builder quá thông minh sẽ che business rule. |
| Object Mother | Tạo nhanh object chuẩn dùng nhiều nơi. | Dễ thành global fixture khổng lồ, thay đổi một default làm hỏng nhiều test. |
| Database fixture | Kiểm tra behavior gần production. | Cần reset strategy, unique key và transaction ownership. |
4. Time, randomness, files và shared state
- Inject
Clockthay vì gọiInstant.now()trực tiếp trong domain logic. - Truyền random seed và ghi seed khi test fail để có thể tái hiện case.
- Dùng temporary directory cho file test và luôn cleanup sau test.
- Đặt locale/timezone trong phạm vi test rồi khôi phục, không thay global state mà không ownership.
- Không phụ thuộc thứ tự method hoặc dữ liệu do test khác tạo.
- Fixture dùng chung phải có cleanup và thread-safety rõ trước khi bật parallel execution.
5. Failure modes
- Assertion quá rộng: lỗi khó định vị vì một snapshot hoặc object dump chứa quá nhiều field.
- Snapshot/golden file cập nhật mù quáng: test xanh nhưng regression được chấp nhận cùng với snapshot mới.
- Fixture quá lớn: test không cho biết field nào thật sự quan trọng với behavior.
- Money comparison không nhất quán: scale hoặc rounding khác nhau tạo false failure hoặc che sai số.
- Implementation coupling: refactor nội bộ làm test vỡ dù observable behavior không đổi.
- Leaked shared state: test pass riêng lẻ nhưng fail khi chạy cả suite hoặc chạy song song.
6. Câu hỏi phỏng vấn đào sâu
- Khi nào interaction verification là contract thật thay vì implementation detail?
- Vì sao builder tốt hơn Object Mother trong một suite lớn?
- Làm sao kiểm tra exception mà vẫn chứng minh state không bị thay đổi?
- Test của bạn sẽ kiểm soát clock, random seed và timezone như thế nào?
- Tại sao test snapshot có thể tạo cảm giác an toàn giả?
7. Checklist tự đánh giá
- Tôi viết assertion cho observable behavior và invariant.
- Tôi phân biệt được fixture phục vụ intent với fixture che mất intent.
- Tôi có chiến lược cho clock, randomness, locale, files và shared state.
- Tôi không gọi private method chỉ để tăng coverage.
- Tôi có thể giải thích vì sao một interaction là contract quan trọng.