How to Use Stubs, Fakes, Spies, and Mocks in Python Testing
Implement four types of test doubles — stubs, fakes, spies, and mocks — as subclasses of a PaymentGateway interface to replace real dependencies during testing.
Python code
73 linesclass PaymentGateway:
def charge(self, amount):
raise NotImplementedError
class StubPaymentGateway(PaymentGateway):
"""Returns a fixed response without any logic."""
def charge(self, amount):
return {"success": True, "transaction_id": "stub-12345"}
class FakePaymentGateway(PaymentGateway):
"""Simulates the real system with in-memory storage."""
def __init__(self):
self.transactions = []
def charge(self, amount):
self.transactions.append(amount)
return {"success": True, "transaction_id": f"fake-{len(self.transactions)}"}
class SpyPaymentGateway(PaymentGateway):
"""Records calls for later assertions."""
def __init__(self):
self.called_with = []
def charge(self, amount):
self.called_with.append(amount)
return {"success": True, "transaction_id": "spy-0001"}
class MockPaymentGateway(PaymentGateway):
"""Pre-programmed with expectations and verifies them."""
def __init__(self, expected_amount):
self.expected_amount = expected_amount
self.called = False
def charge(self, amount):
self.called = True
assert amount == self.expected_amount, f"Expected {self.expected_amount}, got {amount}"
return {"success": True, "transaction_id": "mock-0001"}
def verify(self):
assert self.called, "charge() was never called"
def process_order(gateway, amount):
result = gateway.charge(amount)
if result["success"]:
print(f"Order of ${amount} processed successfully")
if __name__ == "__main__":
# Stub - just returns fixed data
stub = StubPaymentGateway()
process_order(stub, 100)
# Fake - real in-memory behavior
fake = FakePaymentGateway()
process_order(fake, 50)
process_order(fake, 75)
print(f"Fake recorded transactions: {fake.transactions}")
# Spy - records calls
spy = SpyPaymentGateway()
process_order(spy, 30)
print(f"Spy was called with: {spy.called_with}")
# Mock - verifies specific expected behavior
mock = MockPaymentGateway(expected_amount=200)
process_order(mock, 200)
mock.verify()
print("Mock verified successfully")
Output
Order of $100 processed successfully
Order of $50 processed successfully
Order of $75 processed successfully
Fake recorded transactions: [50, 75]
Order of $30 processed successfully
Spy was called with: [30]
Order of $200 processed successfully
Mock verified successfully
How it works
Test doubles are replacements for real dependencies that give you control over behavior and allow assertions. A stub returns fixed data to support the code under test, so you can focus on the code path you're testing. A fake is a working implementation, like an in-memory list, that mimics the real system's behavior. A spy records how it was called so you can verify interactions that happened. A mock is pre-programmed with expected calls and verifies them at the end, making it the strictest of the four. Each double serves a distinct purpose, and choosing the right one keeps your tests fast, isolated, and readable.
Common mistakes
- Using mocks for everything instead of simpler stubs or fakes
- Over-asserting on implementation details with spies instead of verifying behavior
- Forgetting to call verify() on mocks, leaving expectations unchecked
- Mixing real logic into stubs, which then can't be trusted as fixed fixtures
Variations
- Use unittest.mock.Mock or MagicMock for quick mocking without custom classes
- Use dependency injection to pass test doubles into the code instead of hardcoding them
Real-world use cases
- Replacing a payment gateway in tests to avoid charging real credit cards.
- Simulating an email service with a fake that records sent messages in a list.
- Verifying that a retry wrapper calls an API exactly three times using a spy or mock.
Sponsored
More from Testing & modern typing
- Capture stdout and stderr with pytest capsys easy
- Characterization Test for Legacy Python Code medium
- Dataclass with Type Hints Fields in Python easy
- Dependency Injection in Python for Testability easy
- Design Data Helpers with Python TypedDict and Literal easy
- Fix and Test a Regression Bug in Python with Unit Tests easy
Keep learning
Related tutorials and quizzes for this topic.