Mockito: mock ở biên, không mock mọi thứ

4 câu hỏi~25 phútTừ khóa: test double, mock, stub, fake, spy, when/thenReturn, verify, ArgumentCaptor, @Mock, @InjectMocks, strict stubs, over-mocking, mockStatic

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:

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());

3. Giải pháp: chọn đúng loại test double

LoạiLà gìVí dụ trong shop-app
DummyChỉ để lấp tham số, không dùngnull / object rỗng
StubTrả dữ liệu cố định cho câu hỏi (query)paymentClient.charge() trả success
MockStub + kiểm tra được gọi thế nào (verify)verify events.publishEvent(OrderCreated)
FakeCài đặt thật nhưng đơn giảnInMemoryProductRepository dùng HashMap
SpyObject thật, ghi lại lời gọi, stub một phầnHiếm khi cần, thường là dấu hiệu thiết kế

Quy tắc mock:

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 nhanhRất nhanhChậm hơn (giây)
Độ tin cậyThấp nếu stub sai hành vi thậtTrung bìnhCao
Chi phí bảo trìCao khi over-mockPhải viết, giữ đúng hành viThấp, cần Docker
Hợp vớiBiên: HTTP, broker, emailRepository trong unit testSQL, mapping, transaction

7. Khi nào nên dùng / không nên

@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:

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));

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


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.