Mockito và test doubles tại boundary
Mock là công cụ kiểm soát collaborator ở một boundary có chủ đích, không phải bản sao của hệ thống production và cũng không nên thay thế mọi dependency.
1. Taxonomy và lựa chọn
| Double | Vai trò | Dùng khi | Cạm bẫy |
|---|---|---|---|
| Stub | Trả response định trước. | Điều khiển nhánh logic và failure case. | Có thể bỏ qua behavior thật của dependency. |
| Mock | Ghi và verify interaction. | Interaction chính là contract, ví dụ không charge hai lần. | Dễ couple test với call graph. |
| Fake | Implementation đơn giản nhưng hoạt động. | Cần state/behavior nhẹ, ví dụ in-memory repository. | Semantics có thể khác technology thật. |
| Spy | Bọc object thật và cho phép override một phần. | Legacy seam có chủ đích. | Vô tình gọi code thật hoặc tạo test khó hiểu. |
2. Call flow và strict stubbing
Test tạo mock → khai báo stubbing → system under test gọi collaborator → Mockito ghi invocation → assertion kiểm tra outcome và chỉ verify interaction quan trọng. Strict stubbing giúp phát hiện setup thừa hoặc argument không khớp; lenient không nên được dùng để che design smell.
when(paymentGateway.charge(any())).thenReturn(success());
service.placeOrder(command);
verify(paymentGateway).charge(
argThat(req -> req.idempotencyKey().equals("order-42")));
3. Verify đúng chỗ
- Verify side effect quan trọng: charge payment, publish event, audit record hoặc retry count.
- Không verify getter, mapper hoặc call order nội bộ nếu business outcome đã đủ.
ArgumentCaptorhữu ích khi payload không được trả ra nhưng vẫn là contract; hãy assert field có ý nghĩa thay vì snapshot toàn object.- Không mock value object/entity đơn giản; tạo object thật thường rõ và bền hơn.
any()cho mọi argument làm test không kiểm tra dữ liệu quan trọng.
4. Spy, final/static và async
Spy có thể gọi code thật nếu không stub đúng kiểu. Dùng doReturn(...).when(spy) khi cần tránh lời gọi thật trong lúc stubbing; đừng để spy che một boundary thiết kế chưa rõ.
Mock final/static có thể hữu ích ở legacy seam nhưng làm tăng khả năng khóa implementation. Ưu tiên dependency injectable và architectural boundary rõ ràng.
Async verification với timeout có thể che race condition nếu chỉ “đợi đủ lâu”. Tốt hơn là expose completion signal, awaitility/coordination rõ ràng và assert eventual invariant.
5. Failure modes
- Over-mocking: refactor nội bộ không đổi behavior nhưng hàng loạt test fail vì suite khóa call graph.
- Lenient setup: stubbing thừa bị che, làm test khó đọc và dependency không rõ.
- Mock mọi collaborator: test pass với object graph giả nhưng không chứng minh integration hoặc persistence semantics.
- Verify private sequence: test vỡ khi reorder implementation dù contract không thay đổi.
- Async timeout: test lúc pass lúc fail vì race, không có signal completion hoặc cleanup đúng.
6. Câu trả lời phỏng vấn mẫu
Over-mocking couples tests to implementation details. I mock architectural boundaries or expensive nondeterministic dependencies, not every collaborator. For persistence behavior, SQL semantics, or framework configuration, I prefer an integration test against the real technology.
7. Checklist tự đánh giá
- Tôi phân biệt stub, mock, fake và spy.
- Tôi verify observable behavior trước interaction.
- Tôi biết khi nào nên dùng real technology thay vì mock.
- Tôi không dùng
any()hoặclenientđể che assertion/setup yếu. - Tôi có thể giải thích vì sao một test fail hàng loạt sau refactor là dấu hiệu over-mocking.