Test Doubles: Mocks, Stubs, Spies, Fakes, and Dummies Explained
Everyone calls them mocks. They're not all mocks. Dummy, Stub, Spy, Mock, Fake — these five types of test doubles serve different purposes, and using the wrong one produces tests that either verify nothing or are so tightly coupled to implementation that they break every refactor.
Key Takeaways
"Mock" is not a generic term for test doubles. Mocks verify behavior (was this method called?). Stubs return canned responses. Spies record interactions. Fakes are working implementations. Using the wrong type produces wrong tests.
Stubs are for controlling dependencies; mocks are for verifying interactions. If you're asserting that a method was called, you want a mock. If you just need predictable return values, you want a stub.
Fakes are underused and often the right answer. An in-memory database implementation is more realistic than a mock and faster than a real database — use fakes for dependencies that are slow, not just external.
Over-mocking is a test design smell. If you need 10 mocks to test one function, the function has too many dependencies — the test is telling you to refactor.
Test doubles are objects that stand in for real dependencies during testing. Just as a stunt double stands in for an actor in dangerous scenes, a test double stands in for a real component so you can test code in isolation — without hitting databases, external APIs, or other slow and unpredictable systems.
The term was coined by Gerard Meszaros in his book xUnit Test Patterns. He identified five distinct types of test doubles, each with a specific purpose. In practice, most developers use "mock" to mean all of them, which causes endless confusion.
This guide explains all five types — Dummy, Stub, Spy, Mock, and Fake — with clear definitions, code examples, and rules for when to use each.
What Is a Test Double?
A test double is any object used in a test that replaces a production object for testing purposes. You use test doubles to:
- Isolate the unit under test from its dependencies
- Control inputs — force specific return values or behaviors
- Verify outputs — assert that certain methods were called with certain arguments
- Speed up tests — avoid real network calls, database queries, or file I/O
- Test error scenarios — simulate failures that are hard to trigger in real systems
The Problem Test Doubles Solve
Consider this function:
async function processOrder(orderId) {
const order = await database.getOrder(orderId);
const payment = await paymentGateway.charge(order.total, order.customerId);
await emailService.sendConfirmation(order.customerId, payment.transactionId);
return payment.transactionId;
}
Testing this function without test doubles would:
- Require a real database with test data
- Make real charges to a payment processor
- Send real emails to customers
- Be slow, flaky, and expensive
With test doubles, you replace database, paymentGateway, and emailService with controlled substitutes. Your test runs in milliseconds and never touches external systems.
The Five Types of Test Doubles
| Type | Returns Values? | Verifies Calls? | Has Real Logic? |
|------|----------------|-----------------|-----------------| | Dummy | No | No | No | | Stub | Yes | No | No | | Spy | Yes (real) | Yes | Yes (wraps real) | | Mock | Yes | Yes | No | | Fake | Yes | No | Yes (simplified) |
Dummy Objects
A dummy is the simplest type. It is passed as an argument but never actually used. The test just needs a value to fill a parameter slot.
When to Use Dummies
- Required parameters that have no effect on the behavior being tested
- Filling constructor arguments when only one method is under test
Example
class OrderProcessor {
constructor(database, logger, metrics, auditTrail) {
this.database = database;
this.logger = logger;
this.metrics = metrics;
this.auditTrail = auditTrail;
}
calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
}
// Testing calculateTotal — we don't need database, logger, metrics, or auditTrail
// Dummies fill the required constructor parameters
test('calculates order total correctly', () => {
const dummy = null; // or {} — doesn't matter, it's never called
const processor = new OrderProcessor(dummy, dummy, dummy, dummy);
const items = [
{ price: 10, quantity: 2 },
{ price: 5, quantity: 3 }
];
expect(processor.calculateTotal(items)).toBe(35);
});
Dummies are often null, empty objects, or empty strings. They exist to satisfy method signatures, nothing more.
Stubs
A stub provides canned (pre-programmed) responses to method calls. It does not verify how it was called — it only returns what you tell it to return.
Stubs control indirect inputs: values that your code reads from dependencies.
When to Use Stubs
- Simulating database query results
- Controlling what an API returns
- Forcing specific date/time values
- Testing error handling by returning errors
Example (JavaScript/Jest)
// The function under test
async function getUserDisplayName(userId, userRepository) {
const user = await userRepository.findById(userId);
if (!user) return 'Anonymous';
return `${user.firstName} ${user.lastName}`;
}
// Stub: returns canned data, we don't verify HOW it was called
test('returns full name when user exists', async () => {
const userRepositoryStub = {
findById: jest.fn().mockResolvedValue({
firstName: 'Jane',
lastName: 'Smith'
})
};
const result = await getUserDisplayName('123', userRepositoryStub);
expect(result).toBe('Jane Smith');
});
test('returns Anonymous when user not found', async () => {
const userRepositoryStub = {
findById: jest.fn().mockResolvedValue(null)
};
const result = await getUserDisplayName('999', userRepositoryStub);
expect(result).toBe('Anonymous');
});
Notice: these tests assert on the return value of getUserDisplayName, not on how the stub was called. That is what makes it a stub.
Example (Python/unittest.mock)
from unittest.mock import MagicMock
from your_module import get_user_display_name
def test_returns_full_name():
repo_stub = MagicMock()
repo_stub.find_by_id.return_value = {'first_name': 'Jane', 'last_name': 'Smith'}
result = get_user_display_name('123', repo_stub)
assert result == 'Jane Smith'
def test_returns_anonymous_when_not_found():
repo_stub = MagicMock()
repo_stub.find_by_id.return_value = None
result = get_user_display_name('999', repo_stub)
assert result == 'Anonymous'
Spies
A spy wraps a real object and records information about how it was called — which methods were invoked, with what arguments, how many times. Unlike mocks, spies call through to the real implementation by default.
Spies are good for verifying indirect outputs without fully replacing the real dependency.
When to Use Spies
- Verifying that a method was called without replacing its behavior
- Wrapping a real object to observe usage while keeping real logic
- Auditing calls to third-party libraries
Example (JavaScript/Jest)
class EmailService {
async send(to, subject, body) {
// Real implementation sends email
return await mailgunClient.send({ to, subject, body });
}
}
// Spy: calls through to real implementation but records calls
test('sends welcome email on registration', async () => {
const emailService = new EmailService();
const sendSpy = jest.spyOn(emailService, 'send');
await registerUser({ email: 'user@example.com', name: 'Jane' }, emailService);
expect(sendSpy).toHaveBeenCalledOnce();
expect(sendSpy).toHaveBeenCalledWith(
'user@example.com',
'Welcome to HelpMeTest',
expect.stringContaining('Jane')
);
});
Example (Python/unittest.mock)
from unittest.mock import patch, call
def test_sends_welcome_email():
with patch.object(email_service, 'send', wraps=email_service.send) as spy:
register_user({'email': 'user@example.com', 'name': 'Jane'})
spy.assert_called_once()
assert spy.call_args == call(
'user@example.com',
'Welcome!',
unittest.mock.ANY
)
The key characteristic of a spy: it records calls while still running real code (unlike a mock, which replaces the implementation entirely).
Mocks
A mock is a pre-programmed object with expectations about the calls it will receive. It both provides return values AND verifies that it was called correctly. If the expected calls do not happen, the test fails.
Mocks verify behavior — that your code interacts with dependencies in the expected way.
When to Use Mocks
- Verifying that a notification was sent (not just that the code ran)
- Testing that audit logging records specific events
- Verifying the exact arguments passed to an external service
- Testing that error paths call the right recovery methods
Example (JavaScript/Jest)
// Using mock to verify behavior
test('charges payment and sends confirmation on order', async () => {
const paymentGatewayMock = {
charge: jest.fn().mockResolvedValue({ transactionId: 'txn_123' })
};
const emailServiceMock = {
sendConfirmation: jest.fn().mockResolvedValue(undefined)
};
await processOrder('order_456', paymentGatewayMock, emailServiceMock);
// Verify behavior: payment was charged with correct amount
expect(paymentGatewayMock.charge).toHaveBeenCalledWith('order_456', expect.any(Number));
// Verify behavior: confirmation was sent with transaction ID
expect(emailServiceMock.sendConfirmation).toHaveBeenCalledWith(
expect.any(String),
'txn_123'
);
});
Example (Java/Mockito)
@Test
void processOrder_chargesPaymentAndSendsConfirmation() {
// Arrange
PaymentGateway paymentGatewayMock = mock(PaymentGateway.class);
EmailService emailServiceMock = mock(EmailService.class);
when(paymentGatewayMock.charge(anyString(), anyDouble()))
.thenReturn(new PaymentResult("txn_123"));
OrderProcessor processor = new OrderProcessor(paymentGatewayMock, emailServiceMock);
// Act
processor.processOrder("order_456");
// Assert behavior
verify(paymentGatewayMock).charge("order_456", 99.99);
verify(emailServiceMock).sendConfirmation(anyString(), eq("txn_123"));
}
The distinction from a stub: a mock test fails if charge or sendConfirmation are NOT called. A stub test would only fail if the return value was wrong.
Fakes
A fake has a working implementation, but one that takes shortcuts unsuitable for production. Fakes are more complex than stubs — they contain real logic.
Common Fakes
- In-memory database — stores data in a map/array, perfect for tests, not for production
- In-memory message queue — processes messages synchronously without a broker
- Fake SMTP server — captures sent emails in memory instead of delivering them
- Fake file system — operates on memory buffers instead of disk
When to Use Fakes
- When you need real behavior (e.g., actual data persistence and retrieval) but not production infrastructure
- Integration tests where stubs would be too simplistic
- Local development where spinning up real infrastructure is impractical
Example (In-Memory Repository)
// Production implementation
class PostgresUserRepository {
async save(user) {
return await db.query('INSERT INTO users...', [user]);
}
async findById(id) {
return await db.query('SELECT * FROM users WHERE id = $1', [id]);
}
}
// Fake: real logic, but in-memory storage
class InMemoryUserRepository {
constructor() {
this.users = new Map();
}
async save(user) {
this.users.set(user.id, { ...user });
return user;
}
async findById(id) {
return this.users.get(id) || null;
}
}
// Tests use the fake
test('user registration flow', async () => {
const repo = new InMemoryUserRepository();
const service = new UserService(repo);
await service.register({ id: '1', email: 'test@example.com', name: 'Jane' });
const user = await service.getProfile('1');
expect(user.email).toBe('test@example.com');
});
The fake InMemoryUserRepository actually stores and retrieves data — it has real logic. But it uses a JavaScript Map instead of PostgreSQL.
Fakes vs Stubs
A stub always returns the same canned value regardless of inputs. A fake executes real logic: if you save a user and retrieve it, you get the user back. Stubs cannot do this.
Mock vs Stub: The Key Difference
This is the most common source of confusion. Here is the definitive distinction:
Stubs: State Verification
A stub test checks what state the system is in after running the code. The stub provides inputs; you assert on outputs.
// Stub test — asserting on return value (state)
test('returns discount price for premium users', async () => {
const userServiceStub = {
getUser: jest.fn().mockResolvedValue({ tier: 'premium' })
};
const price = await calculatePrice('product_123', 'user_456', userServiceStub);
expect(price).toBe(80); // 20% discount applied
});
You do not care how getUser was called. You only care that the returned price is correct.
Mocks: Behavior Verification
A mock test checks what the code did — which methods were called, with what arguments. The behavior itself is what you are testing.
// Mock test — asserting on behavior (method calls)
test('logs price calculation for audit', async () => {
const auditLogMock = {
record: jest.fn()
};
await calculatePrice('product_123', 'user_456', {}, auditLogMock);
expect(auditLogMock.record).toHaveBeenCalledWith({
action: 'price_calculated',
productId: 'product_123',
userId: 'user_456'
});
});
You do not care what calculatePrice returns. You care that auditLog.record was called with the right data.
The Rule of Thumb
- Stub when: you are testing the return value or state change of the unit under test
- Mock when: you are testing that the unit under test calls a dependency correctly
Mixing these up leads to brittle tests. If you assert on mock expectations AND return values in the same test, you are usually testing too much at once.
Test Double Frameworks
JavaScript / TypeScript
Jest (built-in) — the most common choice:
const stub = jest.fn().mockReturnValue('value');
const mock = jest.fn();
const spy = jest.spyOn(object, 'method');
Sinon.js — more explicit type distinctions:
const stub = sinon.stub(service, 'getData').returns('value');
const mock = sinon.mock(service).expects('send').once();
const spy = sinon.spy(service, 'log');
Vitest — same API as Jest, faster for Vite-based projects:
const stub = vi.fn().mockReturnValue('value');
const spy = vi.spyOn(object, 'method');
Java
Mockito — the standard for Java unit testing:
UserRepository mock = mock(UserRepository.class);
when(mock.findById("123")).thenReturn(Optional.of(user));
verify(mock, times(1)).save(any(User.class));
EasyMock:
UserRepository mock = createMock(UserRepository.class);
expect(mock.findById("123")).andReturn(Optional.of(user));
replay(mock);
verify(mock);
Python
unittest.mock (standard library):
from unittest.mock import MagicMock, patch, create_autospec
# Stub
repo = MagicMock()
repo.find_by_id.return_value = {'id': '1', 'name': 'Jane'}
# Spy
with patch.object(service, 'send', wraps=service.send) as spy:
service.notify()
spy.assert_called_once()
# Verify calls (mock behavior)
mock_sender = MagicMock()
process_order(mock_sender)
mock_sender.send.assert_called_with(expected_payload)
pytest-mock:
def test_sends_email(mocker):
mock_send = mocker.patch('myapp.email.send')
register_user(email='test@example.com')
mock_send.assert_called_once_with('test@example.com', subject='Welcome')
C# / .NET
Moq:
var mock = new Mock<IUserRepository>();
mock.Setup(r => r.FindById("123")).Returns(new User { Name = "Jane" });
mock.Verify(r => r.Save(It.IsAny<User>()), Times.Once);
NSubstitute:
var sub = Substitute.For<IUserRepository>();
sub.FindById("123").Returns(new User { Name = "Jane" });
sub.Received(1).Save(Arg.Any<User>());
When to Use Each Type
| Situation | Recommended Type |
|---|---|
| Need to fill a required parameter that won't be used | Dummy |
| Need to control what a dependency returns | Stub |
| Need to verify a dependency was called correctly | Mock |
| Need to observe calls without replacing behavior | Spy |
| Need real data storage behavior without real infrastructure | Fake |
| Need to simulate errors and exceptions | Stub |
| Need to test notification/event side effects | Mock |
| Integration tests with real logic but no production systems | Fake |
Decision Flow
Does the test need to verify that a specific method was called?
├── Yes → Mock (or Spy if you want real behavior too)
└── No → Does the dependency need to return specific values?
├── Yes → Stub
└── No → Does the dependency need real logic?
├── Yes → Fake
└── No → Dummy
Common Mistakes
1. Mocking Everything
Over-mocking produces tests that verify implementation details rather than behavior. If you change how a function works internally (but the output stays the same), over-mocked tests break even though nothing is wrong.
Symptom: Every test file has 10+ mock declarations.
Fix: Only mock what you need to control or verify. Use real objects where possible.
2. Confusing Mocks and Stubs
Using jest.fn() for everything is fine mechanically, but if you are asserting on both return values AND call counts in the same test, you are testing two different things and making the test harder to debug.
Fix: Decide whether you are testing state (stub) or behavior (mock) and write a focused test.
3. Brittle Mock Assertions
// Brittle: verifies exact argument structure
expect(mock).toHaveBeenCalledWith({
userId: '123',
timestamp: 1741776000000, // Hard-coded timestamp
action: 'login'
});
Fix: Use asymmetric matchers for values you do not control:
expect(mock).toHaveBeenCalledWith({
userId: '123',
timestamp: expect.any(Number),
action: 'login'
});
4. Testing the Mock, Not the Code
test('sends welcome email', async () => {
const emailMock = jest.fn().mockResolvedValue(undefined);
// This test only proves that calling emailMock works
// It does not test that your code actually calls it
await emailMock('user@example.com');
expect(emailMock).toHaveBeenCalled(); // Always true
});
Fix: Actually invoke the production code being tested, not the mock directly.
5. Using Production Fakes in Production
A fake in-memory database might accidentally end up in production code through a dependency injection misconfiguration. Add assertions or environment checks to prevent this.
6. Not Resetting State Between Tests
Stubs and mocks that are shared between tests carry state from previous tests. In Jest, jest.clearAllMocks() or jest.resetAllMocks() in beforeEach prevents this.
beforeEach(() => {
jest.clearAllMocks(); // Clears call counts and instances
// or
jest.resetAllMocks(); // Also removes return value implementations
});
FAQ
What is the difference between a mock and a stub?
A stub provides canned return values — you use it to control what your code reads from dependencies. A mock verifies that specific methods were called with specific arguments — you use it to test that your code correctly calls its dependencies. Stubs are for state verification; mocks are for behavior verification.
Is jest.fn() a mock or a stub?
Both, depending on how you use it. When you write jest.fn().mockReturnValue('value') and only assert on your function's return value, it is acting as a stub. When you write expect(jestFn).toHaveBeenCalledWith(args), it is acting as a mock. The jest.fn() API supports both patterns.
When should I use a fake instead of a stub?
Use a fake when your tests need real behavior — data that persists between calls within a single test, or logic that stubs cannot replicate with simple return values. A common example: testing a repository pattern where one test saves a record and another test retrieves it. A stub cannot do this; a fake in-memory repository can.
Should I mock the database?
It depends on what you are testing. For pure unit tests of business logic, yes — mock or stub the repository. For testing the repository itself (query logic, data mapping), use a real test database or a fake. For integration tests, use a real database. The key is to match the test double to the scope of what you are testing.
What is spying in testing?
Spying is wrapping a real object to record information about how its methods are called, without replacing the actual implementation. You use spies when you want to verify that your code called a method correctly, but you still want the real implementation to run. This is useful for integration tests or when the real implementation's side effects matter.
What is the difference between a spy and a mock?
A spy wraps a real object and calls through to the real implementation while recording calls. A mock is a complete replacement that returns canned values and does not run any real code. Use a spy when you want real behavior plus observation; use a mock when you want full control of what the dependency does.
How do test doubles relate to dependency injection?
Test doubles require dependency injection (DI) to work. If your code creates its own dependencies with new, you cannot replace them with test doubles. DI allows you to pass dependencies in from outside — making it easy to substitute test doubles in tests. Difficulty using test doubles is often a sign that code needs better DI structure.
Can I use test doubles for external APIs?
Yes — this is one of the primary use cases. Replace your HTTP client with a stub that returns canned API responses. This makes tests fast and reliable. However, ensure you also have integration tests (or contract tests) that verify the real API works as expected, since stubs can drift from reality when the API changes.
Reference: This guide covers one term from the Software Testing Glossary — the complete A–Z reference for every testing concept explained in one place.