Mocking & Stubbing Guide: Test Doubles for Isolated Tests
Mocking and stubbing replace real dependencies with controlled substitutes during testing. They isolate the unit under test from its collaborators — databases, network services, file systems, and other components — making tests faster, more deterministic, and easier to set up. Without test doubles, every test would require a fully configured environment with running services, seeded data, and network connectivity.
Gerard Meszaros introduced the term “Test Double” in his book xUnit Test Patterns as an umbrella for all types of pretend objects used in testing. He defined five specific types — dummies, stubs, fakes, mocks, and spies — each serving a distinct purpose. Understanding these distinctions helps teams choose the right tool for each testing scenario. The Google Testing Blog’s “Testing on the Toilet” series emphasises that over-mocking is one of the most common testing anti-patterns, creating brittle tests that break when implementation details change.
Types of Test Doubles
Stubs
Stubs return predefined values when called. They are used when the test needs the dependency to provide specific data — a user profile, a configuration setting, or a database record. Stubs do not record or verify interactions; they simply return what the test requires. Stubs are the simplest form of test double and are appropriate when you only need the dependency to provide data without caring about how many times it is called or with what arguments.
class PaymentGatewayStub:
def charge(self, amount, card_token):
return {"success": True, "transaction_id": "txn_12345"}
def test_order_confirmation():
gateway = PaymentGatewayStub()
service = OrderService(payment_gateway=gateway)
result = service.place_order(customer_id=1, total=50.00)
assert result["status"] == "confirmed"Mocks
Mocks record the interactions they receive and verify that specific methods were called with the correct arguments. They are used when the test needs to assert that a side effect occurred — an email was sent, a database record was created, or an external API was called. The mock records all calls and makes them available for assertion after the test action completes.
from unittest.mock import Mock
def test_welcome_email_sent():
email_service = Mock()
registration = RegistrationService(email_service)
user = registration.register("alice@test.com", "Alice")
email_service.send.assert_called_once_with(
"alice@test.com",
"Welcome to our platform, Alice!"
)Fakes
Fakes are lightweight working implementations that behave like the real dependency but are simpler. An in-memory database is the most common example. Fakes are more realistic than stubs or mocks and support testing of complex query logic without running a real database server. The xUnit Test Patterns book recommends fakes over mocks when the dependency’s behaviour is complex enough that a mock would require extensive setup to simulate realistic responses.
Spies
Spies wrap real objects and record their interactions without replacing the implementation. They are useful when you want to verify that a method was called on a real object without changing its behaviour.
Mocking Libraries
Python: unittest.mock
Python’s built-in unittest.mock provides Mock, MagicMock, and patch for comprehensive test isolation. MagicMock differs from Mock in that it includes default implementations of magic methods like __len__, __str__, and __iter__, making it suitable for mocking objects used in language-level protocols. The patch context manager temporarily replaces objects in a specified namespace, handling both setup and teardown automatically.
from unittest.mock import patch
@patch('app.services.stripe_service')
def test_payment_retry_on_failure(mock_stripe):
mock_stripe.charge.side_effect = [
ConnectionError("Timeout"),
{"success": True, "id": "txn_retry"}
]
result = PaymentProcessor.process(amount=99.99, card_token="tok_visa")
assert result["success"] is True
assert mock_stripe.charge.call_count == 2JavaScript: Jest
Jest provides built-in mocking through jest.mock(), jest.fn(), and jest.spyOn().
jest.mock('../services/analytics');
const analytics = require('../services/analytics');
test('tracks page view on navigation', () => {
const pageTracker = new PageTracker();
pageTracker.navigate('/products');
expect(analytics.trackPageView).toHaveBeenCalledWith('/products');
expect(analytics.trackPageView).toHaveBeenCalledTimes(1);
---);Java: Mockito
Mockito is the dominant mocking framework for Java, supporting annotations, argument matchers, and verification. Mockito’s @Mock and @InjectMocks annotations reduce boilerplate by automatically creating mocks and injecting them into the class under test. The verify method supports flexible verification modes — times(2), atLeastOnce(), never() — for precise interaction assertions.
import static org.mockito.Mockito.*;
@Test
public void testInventoryCheck() {
InventoryService inventory = mock(InventoryService.class);
when(inventory.checkStock("SKU-123")).thenReturn(42);
OrderService orderService = new OrderService(inventory);
boolean available = orderService.canFulfill("SKU-123", 10);
assertTrue(available);
verify(inventory, times(1)).checkStock("SKU-123");
---When to Mock
Mock external boundaries: third-party APIs you do not control, databases (to avoid state pollution), file systems (to avoid creating test artifacts), network services, and system time. These are the seams where your code meets the outside world. The book Growing Object-Oriented Software, Guided by Tests by Steve Freeman and Nat Pryce popularised the “mockist” approach, arguing that testing through mocked interfaces drives better design by forcing developers to think about role-based interactions rather than implementation details. The alternative “classicist” approach — using real implementations where feasible and mocks only when necessary — is advocated by Kent Beck and Martin Fowler. Both approaches are valid; the choice depends on team preference and project context.
Do not mock value objects, data structures, or internal logic that can be tested through the public API. Mocking what you do not own — standard library functions, third-party library internals — creates tight coupling to implementation details.
Common Mocking Mistakes
- Over-mocking: Mocking everything creates tests that verify mock configurations rather than real behaviour. Only mock at system boundaries.
- Not asserting interactions: A mock that is set up but never verified hides bugs. Always verify that expected interactions occurred.
- Incorrect return types: Mocks that return types different from the real dependency produce misleading test results.
- Mocking implementation details: Tests that mock internal collaborators are brittle and break during refactoring.
Best Practices
Mock at system boundaries where your code meets external dependencies. Use dependency injection to make mocking straightforward. Keep mock setup simple — complex configurations indicate design problems. Use realistic return values that resemble production data. Clean up mocks between tests to prevent state leakage across the test suite. Consider using unittest.mock’s autospec feature to verify that mocked methods match the real API — this prevents the common error of mocking a method that does not exist on the real object. Similarly, Mockito’s mock() with strict stubbing warns when stubbed methods are never called, catching obsolete test setup.
Integration Testing with Real Dependencies
While mocking is essential for unit tests, it should not replace integration testing. Mocks verify that your code calls the right methods with the right arguments. Integration tests verify that the real dependency responds correctly to those calls. The Google Testing Blog’s “Mockist vs. Classicist” debate — whether to mock everything or use real implementations — is resolved by recognising that both approaches serve different testing levels. Use mocks at the unit level and real dependencies at the integration level.
Mocking Frameworks by Language
Different languages have different mocking approaches. Python’s unittest.mock provides MagicMock and patch. Java uses Mockito and EasyMock. JavaScript/TypeScript has Jest mocks, Sinon.js, and vitest mocks. Go uses interfaces and hand-written mocks or gomock. Rust uses trait-based mocking with mockall. Choose a framework that integrates well with your test runner and supports the mocking patterns your code requires.
Partial Mocks and Spies
Partial mocks keep the real implementation but mock specific methods. Spies wrap real objects and track method calls. Both are useful when testing legacy code where you cannot easily inject dependencies. Use them sparingly in new code — they indicate a design that might benefit from refactoring toward explicit dependency injection.
Test Environment Management
Test environments are a common bottleneck in quality assurance. Challenges: environment availability conflicts, configuration drift between environments, data inconsistency, and recreation time. Solutions: use infrastructure-as-code (Terraform, Pulumi, CloudFormation) to provision environments consistently. Containerize test environments with Docker Compose for reproducibility. Use ephemeral environments — spin up for each test run and destroy afterward. Production-like test data is essential: anonymized production data copies, synthetic data generation tools, and data masking for sensitive information. Database migration testing is often overlooked — test that migrations work both forward and backward. Environment monitoring tracks availability and alerts when issues arise.
Exploratory Testing Techniques
Exploratory testing complements automated testing by finding unexpected issues. Session-based test management structures exploratory testing into timed sessions with a charter (mission), notes, and bug reports. Heuristic testing uses rules of thumb to guide exploration: FEW HICCUPS (Functionality, Errors, Workflow, High-volume, Interrupts, Compatibility, Configuration, Usability, Performance, Security). Pair testing — two testers working together — combines different perspectives and catches more issues than solo testing. Charter-based testing defines a mission without detailed test cases, allowing testers to follow their curiosity within bounds.
FAQ
Q: What is the difference between a mock and a stub?
A: A stub returns predefined values without recording interactions. A mock records interactions and verifies that specific methods were called with expected arguments.
Q: When should I use a fake instead of a mock?
A: Use fakes when the dependency’s behaviour is important enough to test through — an in-memory database for query testing, for example. Use mocks when you only need to verify that a call was made.
Q: How do I avoid over-mocking?
A: Mock only at system boundaries — external APIs, databases, file systems. Use real objects for internal logic and value types.
Q: Does mocking make tests slower?
A: No — mocking makes tests faster by replacing slow dependencies like databases and network calls with lightweight in-memory substitutes.
Internal Links
- Read Unit Testing Guide for the AAA pattern and assertion best practices.
- Explore Test Coverage Guide to understand how mock-based tests contribute to coverage metrics.
- Learn about Testing Fundamentals for the role of test doubles in the testing pyramid.