Skip to content

Test Doubles: Stubs, Mocks, Spies and Fakes Explained

DodaTech Updated 2026-06-22 8 min read

In this tutorial, you'll learn about Test Doubles: Stubs, Mocks, Spies and Fakes Explained. We cover key concepts, practical examples, and best practices.

Test doubles are simulated objects that replace real dependencies during testing, letting you isolate the code under test from databases, APIs, file systems, and other external systems that would make tests slow or unreliable.

What You'll Learn

In this tutorial, you'll learn the five types of test doubles — dummies, stubs, spies, mocks, and fakes — with clear examples of when and how to use each one in your test suites.

Why This Matters

Real dependencies make tests slow, flaky, and hard to set up. A database call adds milliseconds, an API call adds seconds, and a file system dependency varies by machine. Using the right test double keeps tests fast, deterministic, and focused on the behavior you care about. Durga Antivirus Pro uses fakes of its virus definition database in unit tests, replacing the real database with an in-memory version that loads in under a millisecond.

Learning Path

flowchart LR
  A[Unit Testing Basics] --> B[Test Doubles
You are here] B --> C[Mocking Best Practices] B --> D[Integration Testing] C --> E[Fakes for Repositories] D --> E style B fill:#f90,color:#fff

The Five Types of Test Doubles

The term "test double" comes from the film industry where a "stunt double" replaces an actor for dangerous scenes. In testing, doubles replace real objects to make testing safer and more predictable.

1. Dummy

A dummy is an object that is passed around but never actually used. It exists only to satisfy parameter lists.

def test_send_email():
    # Dummy objects - passed but never accessed
    dummy_config = object()
    dummy_logger = object()

    result = send_email(
        to="user@example.com",
        subject="Hello",
        body="Test",
        config=dummy_config,
        logger=dummy_logger
    )

    assert result is True

Dummies are the simplest test double. They prevent your code from crashing on missing parameters without adding any behavior.

2. Stub

A stub provides fixed answers to calls made during the test. You use stubs when you need a dependency to return a specific value.

from unittest.mock import Mock

def test_get_user_returns_profile():
    # Stub - returns fixed data
    db_stub = Mock()
    db_stub.query.return_value = [
        {"id": 1, "name": "Alice", "email": "alice"@example".com"}
    ]

    service = UserService(db_stub)
    result = service.get_user(1)

    assert result.name == "Alice"
    assert result.email == "alice@example.com"

Expected output:

test session starts
collecting ... collected 1 item
test_user_service.py .                                         [100%]
1 passed in 0.01s

3. Spy

A spy records information about how it was called. You use spies to verify that certain functions were invoked with the right arguments.

from unittest.mock import Mock

def test_order_triggers_notification():
    notification_spy = Mock()

    order = OrderService(notifier=notification_spy)
    order.place("user_123", ["item_1", "item_2"])

    # Spy verifies the call happened
    notification_spy.send.assert_called_once_with(
        "user_123",
        "Your order has been placed"
    )

Spies are useful when the side effect of a method matters more than its return value. You care that the email was sent, not what send_email returns.

4. Mock

A mock is a stub that also has expectations about how it should be called. Mocks are pre-programmed with expected calls and will fail if the code under test doesn't match those expectations.

from unittest.mock import Mock

def test_payment_processing():
    payment_mock = Mock()
    payment_mock.process.return_value = {"status": "success", "id": "txn_123"}
    payment_mock.process.assert_not_called()

    service = PaymentService(gateway=payment_mock)
    result = service.charge(100, "USD")

    assert result["status"] == "success"
    # Mock verifies the exact call
    payment_mock.process.assert_called_once_with(100, "USD", "charge")

Expected output when the mock expectation fails:

======================================================================
FAIL: test_payment_processing (test_payment.test_payment_processing)
----------------------------------------------------------------------
AssertionError: Expected call: process(100, 'USD', 'charge')
Actual call: process(100, 'EUR', 'charge')

5. Fake

A fake is a lightweight working implementation of a dependency. Unlike stubs and mocks, a fake has actual working behavior but is simplified for testing.

class InMemoryDatabase:
    """A fake database that stores data in memory instead of a real database."""

    def __init__(self):
        self._users = {}
        self._next_id = 1

    def save_user(self, user):
        user.id = self._next_id
        self._users[self._next_id] = user
        self._next_id += 1
        return user

    def find_by_email(self, email):
        for user in self._users.values():
            if user.email == email:
                return user
        return None

def test_register_user():
    db = InMemoryDatabase()  # Fake - real behavior, no database needed
    service = RegistrationService(db)

    user = service.register("alice@example.com", "password123")

    assert user.id == 1
    assert db.find_by_email("alice@example.com") is not None

Expected output:

test session starts
collecting ... collected 1 item
test_registration.py .                                         [100%]
1 passed in 0.01s

Fakes are preferred over mocks when the real dependency is complex but can be reasonably simplified. They make tests more realistic while staying fast.

Comparison Table

Type Purpose Verifies Example
Dummy Fills parameter requirements Nothing object() passed as config
Stub Returns fixed values Nothing mock.return_value = 42
Spy Records calls Call arguments mock.assert_called_with(x)
Mock Pre-programmed expectations Call count and args mock.assert_called_once()
Fake Working but simplified Actual behavior In-memory database

When to Use Each Type

Scenario Recommended Double
Method requires unused parameter Dummy
Dependency returns a fixed value Stub
Need to verify a side effect occurred Spy
Need strict call expectations Mock
Dependency has complex behavior Fake

Common Errors

1. Using Mocks When Stubs Would Do

If you don't need to verify call patterns, use a stub. Mocks add unnecessary coupling to implementation details.

2. Implementing Fakes with Full Production Logic

A fake should be simpler than the real thing. If your fake is as complex as the production code, it will have the same bugs.

3. Mixing Stub and Mock Behavior Incorrectly

A mock is a stub plus expectations. Don't use a plain stub when you need call verification, and don't use a mock when you only need return values.

4. Forgetting to Reset Spies Between Tests

Spies that aren't reset carry call data across tests, causing false positives or false negatives. Always reset or recreate them per test.

5. Testing Implementation Through Mocks

Mocks should verify that the right messages were sent to collaborators, not that the collaborator processed them in a specific order internally.

Practice Questions

1. What is the difference between a stub and a mock? A stub returns fixed values without expectations. A mock has pre-programmed expectations about how it should be called and asserts against those expectations.

2. When would you use a fake instead of a mock? Use a fake when the dependency's behavior matters for the test, not just its return value or call pattern. Fakes provide realistic behavior without the overhead of the real implementation.

3. What does a spy record? A spy records how it was called — which arguments were passed, how many times, and in what order. It does not change the behavior of the original method.

4. What is the simplest type of test double? A dummy. It satisfies parameter requirements but is never actually used in the test.

5. Why are fakes preferred for repository testing? Fakes allow you to test the full interaction pattern (save, find, update, delete) against a realistic implementation without needing a real database server.

Challenge: Take a service class that depends on a database repository, an email service, and a payment gateway. Write tests using the appropriate double for each dependency — a fake for the repository, a spy for the email service, and a stub for the payment gateway.

Real-World Task: User Registration with All Doubles

Build a complete test suite for a user registration system that uses all five double types. The system checks for duplicate emails, validates passwords, creates the user, sends a confirmation email, and logs the activity.

Use different doubles for each:

  • Dummy for the logger (required but not tested)
  • Stub for the email validator (always returns valid)
  • Spy for the email sender (verify it was called)
  • Mock for the audit service (expects specific calls)
  • Fake for the user repository (in-memory storage)

FAQ

What is the difference between a mock and a spy?

A mock has expectations built in and will fail if those expectations aren't met. A spy passively records calls without pre-programmed expectations. You inspect the spy at the end of the test to see what happened.

Can a fake replace a real database in production?

No. Fakes are for testing only. They skip production concerns like persistence, concurrency, and network resilience. Use a real database in integration tests.

Is it okay to use multiple test doubles in one test?

Yes. A single test often uses different doubles for different dependencies. The key is choosing the right type for each dependency based on what you need to verify.

Do test doubles make tests less reliable?

They can if overused. The risk is that your doubles don't match the real behavior of the dependency. This is called the "mock-to-production gap." Test critical paths with real dependencies in integration tests.

What is the difference between test double and mocking framework?

A test double is the concept (a replacement object). A mocking framework like unittest.mock or Jest is a tool that creates doubles programmatically. You can create doubles manually without any framework.

What's Next

Tutorial What You'll Learn
Mocking Best Practices Advanced mocking patterns and pitfalls
Integration Testing Guide Testing with real dependencies
Unit Testing Best Practices Foundation concepts for effective testing

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro