Mocks, Stubs, Spies, and Fakes Compared
Understand the differences between test doubles: mocks, stubs, spies, and fakes, with practical examples in JavaScript and Python.
What you'll learn
- ✓What each type of test double does and when to use it
- ✓The difference between state verification and behavior verification
- ✓Practical examples with Jest, Vitest, and Python unittest.mock
- ✓When over-mocking leads to brittle tests
Prerequisites
- •Basic unit testing experience
- •Familiarity with dependency injection
What Are Test Doubles
When testing a unit of code, you often need to replace its dependencies with controlled substitutes. These substitutes are collectively called test doubles — a term borrowed from stunt doubles in film. The dependency might be a database, an HTTP API, a file system, or another module.
There are four main types: stubs, spies, mocks, and fakes. Developers often use these terms interchangeably, but they have distinct meanings and serve different purposes.
Stubs: Pre-programmed Responses
A stub replaces a dependency with a fixed response. It does not verify how it was called. It simply returns what you tell it to.
Purpose: Control the indirect inputs to the system under test.
JavaScript Example (Jest)
// The dependency
const userRepository = {
findById: jest.fn(),
};
// Stub it to return a specific user
userRepository.findById.mockReturnValue({
id: 1,
name: "Alice",
email: "alice@example.com",
});
// Use it in the test
test("greets the user by name", () => {
const greeting = greetUser(userRepository, 1);
expect(greeting).toBe("Hello, Alice!");
// We only check the OUTPUT, not how findById was called
});
Python Example
from unittest.mock import MagicMock
def test_greets_user_by_name():
repo = MagicMock()
repo.find_by_id.return_value = {"id": 1, "name": "Alice"}
greeting = greet_user(repo, 1)
assert greeting == "Hello, Alice!"
When to use: When you need the dependency to return a specific value so you can test the logic that consumes that value.
Spies: Recording Calls
A spy wraps a real or fake implementation and records every call made to it — arguments, return values, call count. You can inspect these records after the test runs.
Purpose: Verify that your code interacts with the dependency correctly without replacing the dependency’s behavior.
JavaScript Example
test("sends a welcome email after registration", () => {
const emailService = {
send: jest.fn(), // spy that records calls
};
registerUser(emailService, { name: "Alice", email: "alice@example.com" });
// Verify the interaction
expect(emailService.send).toHaveBeenCalledTimes(1);
expect(emailService.send).toHaveBeenCalledWith(
"alice@example.com",
"Welcome!",
expect.stringContaining("Alice")
);
});
Spying on Real Methods (Vitest)
import { vi, test, expect } from "vitest";
import * as analytics from "./analytics";
test("tracks page view", () => {
const spy = vi.spyOn(analytics, "trackEvent");
renderHomePage();
expect(spy).toHaveBeenCalledWith("page_view", { page: "/" });
spy.mockRestore(); // restore original implementation
});
The key difference from a stub: a spy on a real method still calls the original implementation by default. You observe behavior without altering it.
When to use: When you need to verify that a side effect happened (email sent, event tracked, log written) without changing how the dependency works.
Mocks: Pre-programmed Expectations
In strict testing terminology, a mock is a test double with built-in expectations. You define upfront what calls it expects, and the mock itself verifies those expectations at the end of the test.
Purpose: Define and verify interaction contracts in a single step.
Python Example (strict mock)
from unittest.mock import MagicMock, call
def test_processes_order_correctly():
payment_gateway = MagicMock()
inventory = MagicMock()
process_order(payment_gateway, inventory, order_id=42)
# Verify the exact sequence of calls
payment_gateway.charge.assert_called_once_with(amount=99.99, currency="USD")
inventory.reserve.assert_called_once_with(item_id=7, quantity=2)
inventory.confirm.assert_called_once_with(item_id=7, quantity=2)
In practice, the line between mocks and spies is blurry in JavaScript frameworks. Jest’s jest.fn() acts as both a spy (it records calls) and a mock (you can set expectations). The important distinction is conceptual: are you verifying state (the output of your function) or behavior (the calls your function makes)?
Fakes: Working Implementations
A fake is a lightweight but functional implementation of a dependency. Unlike stubs (which return canned data) and mocks (which record interactions), fakes actually work — but they use shortcuts unsuitable for production.
Purpose: Provide a fast, deterministic alternative to a heavy dependency.
In-Memory Database Fake
class FakeUserRepository {
constructor() {
this.users = new Map();
this.nextId = 1;
}
create(user) {
const id = this.nextId++;
const saved = { ...user, id };
this.users.set(id, saved);
return saved;
}
findById(id) {
return this.users.get(id) || null;
}
findAll() {
return [...this.users.values()];
}
delete(id) {
return this.users.delete(id);
}
}
test("creates and retrieves a user", () => {
const repo = new FakeUserRepository();
const user = repo.create({ name: "Alice", email: "alice@example.com" });
expect(repo.findById(user.id)).toEqual({
id: 1,
name: "Alice",
email: "alice@example.com",
});
});
Fake HTTP Server
# A fake that simulates an external API
class FakePaymentGateway:
def __init__(self):
self.charges = []
def charge(self, amount, currency, card_token):
charge_id = f"ch_{len(self.charges) + 1}"
self.charges.append({
"id": charge_id,
"amount": amount,
"currency": currency,
})
return {"id": charge_id, "status": "succeeded"}
def refund(self, charge_id):
return {"id": f"re_{charge_id}", "status": "refunded"}
When to use: When you need realistic behavior across multiple operations. Fakes are excellent for database repositories, caches, message queues, and external APIs that you call frequently in tests.
Comparison Table
| Type | Returns data? | Records calls? | Has real logic? | Verifies interactions? |
|---|---|---|---|---|
| Stub | Yes (canned) | No | No | No |
| Spy | Optional | Yes | Optional | After the fact |
| Mock | Yes (canned) | Yes | No | Yes (built-in) |
| Fake | Yes (computed) | No | Yes (simplified) | No |
State vs. Behavior Verification
The choice between test doubles comes down to what you are verifying:
State verification checks the output or side effects of the system under test. You call the function, then assert on the return value or the state of the world.
test("calculates total with tax", () => {
const taxService = { getRate: () => 0.08 }; // stub
const total = calculateTotal(100, taxService);
expect(total).toBe(108); // verify state
});
Behavior verification checks that the system under test called the right methods on its dependencies with the right arguments.
test("saves the order to the database", () => {
const db = { save: jest.fn() }; // mock/spy
createOrder(db, { item: "book", price: 15 });
expect(db.save).toHaveBeenCalledWith({ item: "book", price: 15 }); // verify behavior
});
Prefer state verification when possible. It produces tests that are more resilient to refactoring. If you rename an internal method, state-based tests keep passing. Behavior-based tests break.
The Over-Mocking Trap
A common mistake is mocking everything. When a test mocks four dependencies and asserts on the exact calls made to each one, you end up testing the implementation rather than the behavior. These tests break on every refactor, even when the code still works correctly.
Signs of over-mocking:
- The test has more mock setup than actual assertions.
- Renaming a method breaks tests even though functionality is unchanged.
- The test mirrors the implementation line by line.
- You cannot understand what the test verifies without reading the source code.
Guidelines to avoid over-mocking:
- Mock at architectural boundaries (database, network, file system), not between classes in the same module.
- Prefer fakes over mocks for complex dependencies.
- If you are mocking a function you wrote and own, consider testing at a higher level instead.
- Use stubs for queries (things that return data) and spies/mocks for commands (things that cause side effects).
Choosing the Right Double
- Need a dependency to return a specific value? Use a stub.
- Need to verify a side effect happened? Use a spy.
- Need to verify exact interaction sequences? Use a mock (sparingly).
- Need realistic behavior across multiple operations? Use a fake.
- Not sure? Start with the simplest option (stub or fake) and only add interaction verification when the behavior you are testing is the interaction.
Key Takeaways
Test doubles are essential for isolating units under test, but each type serves a different purpose. Stubs control inputs, spies observe outputs, mocks verify interactions, and fakes provide working alternatives. Prefer state verification and fakes for robust tests. Reserve behavior verification with mocks and spies for side effects at architectural boundaries. Over-mocking creates fragile tests that test implementation instead of behavior — the most common testing anti-pattern in codebases with high coverage but low confidence.
Related articles
- Testing Mocking Strategies for Unit Tests
Learn when and how to mock dependencies in unit tests. Covers stubs, spies, fakes, mock patterns, and how to avoid over-mocking.
- Python Python Mocking with unittest.mock: Complete Guide
Master Python's unittest.mock library for testing. Learn Mock, MagicMock, patch, side_effect, and spec with real-world examples for unit testing.
- Java Mockito Tutorial: Mocking Dependencies in Java Tests
Learn how to use Mockito to write fast, focused unit tests. Understand mocks, stubs, spies, argument captors, and how to avoid over-mocking.
- Testing Integration Testing: Test Real Dependencies, Not Mocks
Learn how to write effective integration tests that verify real database queries, API calls, and service interactions using testcontainers and proper test infrastructure.