The Difference Between a Mock, a Stub, and a Fake That Actually Matters
by Eric Hanson, Backend Developer at Clean Systems Consulting
Why the Distinction Matters
Gerard Meszaros coined the term "test double" in xUnit Test Patterns to cover the full category of objects used in tests to replace real dependencies. Mock, stub, fake, spy, and dummy are all test doubles — but they play different roles and have different implications for test design.
Using the wrong one does not usually break the test, but it often makes the test harder to read, more brittle, or less useful. The distinction matters most when you are deciding what to verify.
Stub: Control Indirect Inputs
A stub provides predetermined responses to calls made during the test. It controls the indirect inputs to the code under test. The test does not assert on the stub — it uses the stub to put the code into a specific state.
# Stub: controls what the code receives from a dependency
def test_applies_correct_tax_for_california():
tax_stub = Mock()
tax_stub.get_rate.return_value = 0.085 # CA rate
calculator = PriceCalculator(tax_service=tax_stub)
result = calculator.total(price=100.0, region="CA")
assert result == 108.50 # The assertion is on the output, not the stub
The stub's job is to make get_rate return a value you control. The test asserts on the output of PriceCalculator, not on what tax_stub was called with. The stub enables the test condition; it is not what is being tested.
Mock: Verify Indirect Outputs
A mock is used to verify that the code under test made specific calls to a dependency. The assertion is on the mock itself. This is appropriate when the behavior you are testing is the side effect — the call that was made — rather than a return value.
# Mock: verifies that a call was made (the side effect IS the behavior)
def test_sends_confirmation_email_after_successful_order():
email_mock = Mock()
order_service = OrderService(email_service=email_mock)
order_service.place_order(customer_id=1, items=[item_a])
email_mock.send_confirmation.assert_called_once_with(
to="customer@example.com",
order_id=ANY
)
This is a legitimate use of a mock: the observable behavior of place_order includes sending a confirmation email. The email is a side effect with no return value to assert on, so verifying the call is the right approach.
The risk with mocks: overusing them for internal implementation details rather than observable side effects. If you are mocking calls that are internal wiring — one method calling another within a service — those mocks will break every time you refactor, even when the behavior is correct.
Fake: A Working Implementation
A fake is a real implementation that is simplified for test use. It does not use predetermined responses — it implements actual logic, just without the infrastructure overhead.
// Fake: real implementation, no I/O
type InMemoryUserRepository struct {
users map[int64]*User
nextID int64
}
func (r *InMemoryUserRepository) Save(user *User) (*User, error) {
r.nextID++
user.ID = r.nextID
r.users[user.ID] = user
return user, nil
}
func (r *InMemoryUserRepository) FindByEmail(email string) (*User, error) {
for _, u := range r.users {
if u.Email == email {
return u, nil
}
}
return nil, ErrNotFound
}
Fakes are more work to create but produce more robust tests. Tests using a fake verify real behavior — the FindByEmail logic actually runs, and a test that calls Save followed by FindByEmail exercises the contract between them. Tests using mocks verify interaction patterns, which are fragile under refactoring.
Use fakes for dependencies you own and will maintain. Use stubs and mocks for dependencies you do not control (external APIs, third-party libraries) or for dependencies with expensive infrastructure requirements.
Spy: Record and Verify Selectively
A spy is a real object that records calls made to it so they can be verified afterward. It differs from a mock in that it delegates to the real implementation — it is a wrapper, not a replacement.
// Spy wraps a real object, records interactions
NotificationService realService = new NotificationService(smtpConfig);
NotificationService spy = Mockito.spy(realService);
orderService.processOrder(order);
// Verify call was made, but real implementation also ran
verify(spy).sendConfirmation(order.getCustomerId());
Spies are useful when you want to verify a call was made but also want the real implementation to execute — for integration-style tests where you need both the side effect and the verification.
The Quick Decision Guide
- Stub when you need to control what a dependency returns to your code
- Mock when the test is about verifying a side effect (a call that was made)
- Fake when you own the dependency and want tests that survive refactoring
- Spy when you need both real execution and interaction verification
When in doubt: prefer fakes for dependencies you own, stubs for dependencies you do not, and mocks only when the side effect is what you are actually testing.