Mockito: mock ở biên, không mock mọi thứ
Tiếp đi: Mockito. OrderService có 5 dependency, mình định @Mock hết cả 5 cho nhanh. Vậy có ổn không?
Chạy được, nhưng thường là cách tạo ra test giòn nhất. Trước hết cần phân biệt các loại test double, rồi mới quyết định cái nào mock.
1. Bài toán: unit test createOrder
OrderService (bài 5.1) phụ thuộc: ProductRepository, OrderRepository, DiscountPolicy, PaymentClient, ApplicationEventPublisher. Ta muốn test nhanh, không DB, không mạng:
- Hết hàng → ném
OutOfStockException, không charge tiền. - Thành công → charge đúng số tiền sau giảm giá, publish
OrderCreated. - Payment fail → ném
PaymentFailedException, không publish event.
2. Vấn đề khi mock tất cả
when(discountPolicy.apply(any(), any())).thenReturn(Money.of(180));
when(productRepository.findById(1L)).thenReturn(Optional.of(product));
when(orderRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
...
verify(productRepository).findById(1L);
verify(discountPolicy).apply(Money.of(200), VIP);
verify(orderRepository).save(any());
- Mock
DiscountPolicy(logic thuần): test không còn kiểm tra giá tính đúng, chỉ kiểm tra "service gọi hàm". Sai công thức vẫn xanh. - Test lặp lại y nguyên code production → refactor là đỏ (bài 5.1).
- Setup dài hơn cả code được test: over-mocking smell.
3. Giải pháp: chọn đúng loại test double
| Loại | Là gì | Ví dụ trong shop-app |
|---|---|---|
| Dummy | Chỉ để lấp tham số, không dùng | null / object rỗng |
| Stub | Trả dữ liệu cố định cho câu hỏi (query) | paymentClient.charge() trả success |
| Mock | Stub + kiểm tra được gọi thế nào (verify) | verify events.publishEvent(OrderCreated) |
| Fake | Cài đặt thật nhưng đơn giản | InMemoryProductRepository dùng HashMap |
| Spy | Object thật, ghi lại lời gọi, stub một phần | Hiếm khi cần, thường là dấu hiệu thiết kế |
Quy tắc mock:
- Mock: thứ ở biên hệ thống, chậm hoặc không kiểm soát được: HTTP client (
PaymentClient), message broker, email,Clock. - Không mock: value object (
Money), entity (Product,Order), logic thuần (DiscountPolicy) → dùng object thật. - Repository: dùng fake in-memory cho unit test, hoặc test thật bằng
@DataJpaTest+ Testcontainers (5.4, 5.5). Mock repository chỉ khi chỉ cần một stub đơn giản. - Không mock kiểu mình không sở hữu một cách chi tiết (ví dụ
RestClientfluent API). Bọc nó trongPaymentClientcủa mình rồi mock interface đó.
4. Code từng bước
Bước 1: Dependency. mockito-core và mockito-junit-jupiter có sẵn trong spring-boot-starter-test.
Bước 2: Fake repository
class InMemoryProductRepository implements ProductRepository { // interface riêng của domain
private final Map<Long, Product> data = new HashMap<>();
public Optional<Product> findById(Long id) { return Optional.ofNullable(data.get(id)); }
public Product save(Product p) { data.put(p.getId(), p); return p; }
}
Bước 3: Code test
@ExtendWith(MockitoExtension.class) // strict stubs: stub thừa → test fail
class OrderServiceTest {
@Mock PaymentClient paymentClient;
@Mock ApplicationEventPublisher events;
@Captor ArgumentCaptor<OrderCreated> eventCaptor;
InMemoryProductRepository products = new InMemoryProductRepository();
InMemoryOrderRepository orders = new InMemoryOrderRepository();
OrderService service;
@BeforeEach
void setUp() {
service = new OrderService(products, orders, new DiscountPolicy(), paymentClient, events);
products.save(new Product(1L, "Keyboard", Money.of(100), 10));
}
@Test
void chargesDiscountedTotal_andPublishesEvent() {
when(paymentClient.charge(anyLong(), any())).thenReturn(PaymentResult.ok());
Order order = service.createOrder(new CreateOrderCommand(42L, 1L, 2, CustomerTier.VIP));
verify(paymentClient).charge(order.getId(), Money.of(180)); // lệnh ra biên
verify(events).publishEvent(eventCaptor.capture());
assertThat(eventCaptor.getValue().orderId()).isEqualTo(order.getId());
assertThat(products.findById(1L).get().getStock()).isEqualTo(8);
}
@Test
void outOfStock_doesNotCharge() {
var cmd = new CreateOrderCommand(42L, 1L, 99, CustomerTier.REGULAR);
assertThatThrownBy(() -> service.createOrder(cmd)).isInstanceOf(OutOfStockException.class);
verifyNoInteractions(paymentClient, events);
}
@Test
void paymentFailed_noEvent() {
when(paymentClient.charge(anyLong(), any())).thenReturn(PaymentResult.declined("NO_FUNDS"));
assertThatThrownBy(() -> service.createOrder(new CreateOrderCommand(42L, 1L, 1, CustomerTier.VIP)))
.isInstanceOf(PaymentFailedException.class);
verify(events, never()).publishEvent(any());
}
}
Một số API khác hay gặp:
when(paymentClient.charge(anyLong(), any()))
.thenThrow(new PaymentTimeoutException()) // lần 1
.thenReturn(PaymentResult.ok()); // lần 2 (test retry)
doThrow(new IllegalStateException()).when(events).publishEvent(any()); // method void
verify(paymentClient, times(1)).charge(anyLong(), any());
// Matcher: dùng matcher thì TẤT CẢ tham số phải là matcher:
verify(paymentClient).charge(eq(7L), any()); // đúng
// verify(paymentClient).charge(7L, any()); // sai: InvalidUseOfMatchersException
Bước 4: Chạy
./mvnw test -Dtest=OrderServiceTest
# Tests run: 3, Failures: 0 — dưới 1 giây, không cần Spring context
5. Cách hoạt động
mock(PaymentClient.class)
│ Mockito sinh subclass/proxy lúc runtime (ByteBuddy, inline mock maker)
▼
PaymentClient$MockitoMock
├─ mọi lời gọi được GHI LẠI (invocation list) → dùng cho verify
└─ trả giá trị:
có stub khớp? → thenReturn / thenThrow / thenAnswer
không? → default: null, 0, false, Optional.empty(), list rỗng
when(mock.charge(1L, x)) ← thực ra GỌI mock 1 lần, Mockito nhớ "lời gọi cuối"
.thenReturn(ok) ← gắn kết quả vào lời gọi đó
Từ Mockito 5, mock maker mặc định là inline, nên mock được cả class/method final. Static thì dùng mockStatic:
try (MockedStatic<UUID> m = mockStatic(UUID.class)) { // chỉ có hiệu lực trong try
m.when(UUID::randomUUID).thenReturn(FIXED_UUID);
...
}
Cần mockStatic thường là dấu hiệu nên inject dependency (Clock, IdGenerator) thay vì gọi static.
6. So sánh
| Mock (Mockito) | Fake (in-memory) | Thật (Testcontainers) | |
|---|---|---|---|
| Tốc độ | Rất nhanh | Rất nhanh | Chậm hơn (giây) |
| Độ tin cậy | Thấp nếu stub sai hành vi thật | Trung bình | Cao |
| Chi phí bảo trì | Cao khi over-mock | Phải viết, giữ đúng hành vi | Thấp, cần Docker |
| Hợp với | Biên: HTTP, broker, email | Repository trong unit test | SQL, mapping, transaction |
7. Khi nào nên dùng / không nên
- Nên mock: dịch vụ ngoài, lỗi khó tái hiện (timeout, 500), thứ có tác dụng phụ (gửi email, charge tiền).
- Không mock: value object, entity, DTO, logic thuần, collection.
- Không mock để test query:
when(repo.findByStatus(...))không chứng minh query đúng. - Thấy cần mock 6–7 thứ: class đang làm quá nhiều việc, tách ra trước đã.
@SpringBootTest cho mọi test thì suite chạy 10 phút. Cần test slice.@InjectMocks tiện thế, sao bạn lại tự new OrderService(...) trong setUp?
Đúng là tiện, nhưng @InjectMocks có vài điểm khó chịu:
- Nó im lặng khi không inject được: thêm dependency mới vào constructor mà chưa có
@Mock→ nó truyềnnull, test lỗi NPE ở chỗ khó hiểu. - Không trộn được object thật (fake repository,
DiscountPolicythật) một cách rõ ràng. - Gọi constructor bằng tay thì compiler báo ngay khi constructor đổi, và người đọc thấy rõ test dùng gì thật, gì giả.
Với class chỉ có 1–2 mock, @InjectMocks vẫn ổn. Constructor injection (bài 4.1.1) là thứ làm cho việc tự new dễ dàng.
ArgumentCaptor với argThat khác nhau chỗ nào? Khi nào dùng cái nào?
Cả hai đều kiểm tra tham số truyền vào mock, khác ở cách viết assertion:
// argThat: điều kiện ngắn, viết ngay trong verify
verify(events).publishEvent(argThat((OrderCreated e) -> e.orderId() == 7L));
// fail → message chung chung "Argument(s) are different"
// Captor: bắt object ra rồi assert bằng AssertJ, message rõ, assert nhiều field
verify(events).publishEvent(eventCaptor.capture());
assertThat(eventCaptor.getValue())
.extracting(OrderCreated::orderId, OrderCreated::total)
.containsExactly(7L, Money.of(180));
- Điều kiện đơn giản, một field →
argThathoặceq. - Object phức tạp, nhiều field, hoặc gọi nhiều lần (
getAllValues()) → Captor. - Không dùng Captor trong
when(...)(stub): dùng choverify.
Phỏng vấn hỏi Mockito và mock thì trả lời sao?
Q1. Mock, stub, fake, spy khác nhau thế nào?
🇻🇳 Tiếng Việt:
Stub trả dữ liệu cố định cho lời gọi. Mock là stub nhưng còn kiểm tra được nó được gọi thế nào. Fake là cài đặt thật nhưng đơn giản, như repository dùng HashMap. Spy bọc object thật, ghi lại lời gọi và cho stub một phần. Em ưu tiên fake hoặc object thật, mock cho biên hệ thống.
🇬🇧 English:
A stub returns canned answers. A mock is a stub that can also verify how it was called. A fake is a simple working implementation, like an in-memory repository backed by a HashMap. A spy wraps a real object, records calls and allows partial stubbing. I prefer fakes or real objects and use mocks for system boundaries.
Q2. Khi nào nên mock và khi nào không?
🇻🇳 Tiếng Việt:
Mock thứ ở biên, chậm hoặc có tác dụng phụ: HTTP client gọi payment, broker, email, Clock. Không mock value object, entity, logic thuần, vì dùng object thật vừa đơn giản vừa kiểm tra đúng hơn. Không mock repository để test query, phải test với DB thật. Phải mock quá nhiều là dấu hiệu class làm quá nhiều việc.
🇬🇧 English:
I mock things at the boundary that are slow or have side effects: the payment HTTP client, message brokers, email, the Clock. I do not mock value objects, entities or pure logic, because real objects are simpler and test more. I never mock a repository to test a query; that needs a real database. Needing many mocks is a sign the class does too much.
Q3. Over-mocking gây hại gì?
🇻🇳 Tiếng Việt:
Test lặp lại implementation: verify từng lời gọi nội bộ nên refactor là đỏ, trong khi stub sai hành vi thật thì bug vẫn lọt. Setup dài, khó đọc. Em chỉ verify lệnh ra biên như charge payment hay publish event, còn lại assert kết quả.
🇬🇧 English:
The tests mirror the implementation: verifying every internal call makes them break on refactoring, while stubs that do not match real behaviour let bugs through. Setup becomes long and hard to read. I only verify outgoing commands at the boundary, like charging a payment or publishing an event, and assert on results otherwise.
Cách nhớ ngắn gọn
- Stub = trả lời; Mock = trả lời + kiểm tra lời gọi; Fake = bản thật đơn giản.
- Mock ở biên; value object, entity, logic thuần dùng đồ thật.
- Verify lệnh ra ngoài, assert kết quả cho phần còn lại.
- Cần mockStatic → nên inject dependency.
Tự kiểm tra
Mock không có stub, gọi method trả List thì nhận gì?
List rỗng (giá trị mặc định), không phải null.
Vì sao không nên mock DiscountPolicy khi test OrderService?
Nó là logic thuần, nhanh; mock đi thì test không kiểm tra được giá đúng.
Stub method void ném exception thế nào?
doThrow(ex).when(mock).method(...).
verify(mock).charge(7L, any()) lỗi gì?
Trộn giá trị thật với matcher; phải dùng eq(7L).
Strict stubs của MockitoExtension làm gì?
Báo lỗi UnnecessaryStubbingException khi stub không được dùng, giữ test gọn.