Glossary · Testing fundamentals
Test double
Also called: test doubles, test stub, test stubs, mock object
Definition
Any object that stands in for a real dependency in a test, such as a dummy, stub, fake, spy or mock, so the code under test runs fast and predictably.
A test double replaces a real dependency, such as a payment gateway, database, clock or email service, with something the test controls. The name, popularized by Gerard Meszaros, comes from the stunt double in films. Doubles make tests fast and deterministic, and let you simulate situations that are hard to produce for real, like a timeout or a declined card.
The five kinds
| Type | What it does | Example |
|---|---|---|
| Dummy | Passed only to fill a parameter, never used | A null or empty logger the method requires but this test never touches |
| Stub | Returns canned answers to calls | rates.getRate("EUR") always returns 0.92 |
| Spy | A stub that also records how it was called | Records that send() was called once with a given address |
| Mock | Pre-programmed with expectations, and the test verifies the interactions | Verify gateway.charge(4999) was called exactly once |
| Fake | A working but simplified implementation | An in-memory repository backed by a HashMap |
In everyday speech “mock” is used for all of these, and libraries like Mockito create objects that can act as stubs, spies or mocks. Knowing the distinction still helps, especially stub vs mock: a stub supplies inputs to the code under test (state verification), while a mock checks the outputs the code sends to its dependencies (behavior verification).
Example (JUnit 5 with Mockito)
@Test
void declinedCardDoesNotSendConfirmation() {
PaymentGateway gateway = mock(PaymentGateway.class);
EmailService email = mock(EmailService.class);
when(gateway.charge(4999)).thenReturn(PaymentResult.DECLINED); // stub
var checkout = new Checkout(gateway, email);
var result = checkout.placeOrder(4999);
assertEquals(OrderStatus.PAYMENT_FAILED, result.status());
verify(email, never()).sendConfirmation(any()); // mock-style verification
}
(Static imports from org.mockito.Mockito, org.mockito.ArgumentMatchers and org.junit.jupiter.api.Assertions are omitted.)
Common mistakes
- Mocking everything. Tests that verify every internal call break on every refactor and can pass while the real integration is broken. Mock at architectural boundaries; prefer real objects or fakes inside them.
- Doubles that drift from reality. A stubbed API that returns a field the real service renamed months ago gives false confidence. Contract tests and a few real integration tests keep doubles honest.
- Mocking types you do not own. Wrap third-party clients in your own interface and double that instead.