29 · 32 · 5 min
Standing in for a real collaborator
A class under test usually depends on something slow, external or unpredictable. A test double replaces it, and there are several kinds people lump together as "mocks".
The kinds
| Kind | Does |
|---|---|
| Dummy | fills a parameter, never used |
| Stub | returns canned answers |
| Fake | a real but simplified implementation |
| Spy | records what happened, for inspection after |
| Mock | expects specific calls, fails if they do not happen |
// stub: supplies an answer
when(rates.forToday()).thenReturn(new Rate("USD", 83.2));
// fake: a working in memory implementation
var repository = new InMemoryOrderRepository();
// mock: asserts an interaction
verify(email).send("[email protected]", "Order confirmed");Prefer fakes
An in memory repository implementing the same interface is usually better than a stubbed one. It behaves consistently across many tests, it catches misuse a stub would happily accept, and it does not need re-stubbing every time the test changes.
Stubbing every call individually produces tests that restate the implementation, and those break on refactoring.
Verify outcomes, not calls
// coupled to how it is done
verify(repository).save(any(Order.class));
// coupled to what happens
placeOrder(order);
assertTrue(repository.findById(order.id()).isPresent());The second survives a change from save to saveAll. The first does not, while also passing if save was called with the wrong data in a way the matcher tolerates.
The legitimate exception is when the call is the behaviour. That a payment gateway was charged exactly once is a real guarantee, and verifying it is correct.
Do not mock what you do not own
Mocking a third party client encodes your belief about how it behaves. If that belief is wrong, the test passes and production fails, and the test will keep passing after the library changes.
The better approach is a thin adapter of your own around the library, mocked freely, with a small number of integration tests exercising the real thing.
Over mocking
A test with ten mocks is usually telling you the class has ten collaborators. The fix is the class, not the test. And a test where everything is mocked verifies only that the code calls the methods it calls, which is true by construction.