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.

Medium Python 3.9+ Aug 9, 2026 Testing & modern typing 12 views 0 copies

Python code

73 lines
Python 3.9+
class 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

stdout
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

  1. Use unittest.mock.Mock or MagicMock for quick mocking without custom classes
  2. 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

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Testing & modern typing

Related tutorials and quizzes for this topic.