Why tests replace dependencies

The Portal team's cost page needs four things besides its own logic: hourly prices from a market price service (for customers on a dynamic contract), the 15-minute readings from the reading repository (PostgreSQL in production), a notifier that sends an SMS or email when a reading looks wrong, and an audit log. A unit test that uses all four for real would be slow, would fail whenever the price service is down, could send a real SMS to a real customer, and could not easily produce the situation it wants to test, such as a price of exactly €0.1910 at 23:00 or a timeout.

A test double is an object that takes the place of such a dependency during a test. The term, after a stunt double in a film, comes from Gerard Meszaros' book xUnit Test Patterns (2007). The Java page shows the idea with a hand-written class; this page covers the kinds of double, the Python tools, and when doubles help or hurt.

The code under test, DailyCost, in the middle, with its four dependencies replaced by test doubles: a stub instead of the market price API, a fake instead of the PostgreSQL reading repository, a spy instead of the SMS gateway, and a dummy for the audit log that these tests never use. DailyCost code under test Stub: StubPrices not the market API Fake: FakeReadings not PostgreSQL Spy: SpyNotifier not the SMS gateway Dummy: object() audit log, never used
In the test, every dependency of DailyCost is a double; the logic in the middle is real.

Replacing a dependency is easiest when the code receives it from outside instead of creating it itself: DailyCost gets its prices, readings, notifier and audit log through its constructor. That is dependency injection, and it is the main reason testable code looks the way it does.

The five kinds of test double

Martin Fowler's summary of Meszaros' vocabulary (his "TestDouble" article of 2006, and "Mocks Aren't Stubs", first published in 2004) names five kinds. In everyday speech people call all of them "mocks", which causes real misunderstandings in reviews; the difference is in what the double does and what the test checks. Here are four of them in one test file, written by hand as ordinary Python classes:

from datetime import date, datetime
from decimal import ROUND_HALF_UP, Decimal


# ---- code under test ----------------------------------------------------------
class DailyCost:
    """Cost of one day for a customer on a dynamic (hourly price) contract."""

    def __init__(self, prices, readings, notifier, audit_log):
        self.prices = prices          # market price service, over the network
        self.readings = readings      # reading repository, PostgreSQL in production
        self.notifier = notifier      # sends an SMS or email to the customer
        self.audit_log = audit_log    # only used when a price is corrected

    def total(self, meter_id, day):
        total = Decimal("0")
        for at, kwh in self.readings.for_day(meter_id, day):
            if kwh > 25:
                self.notifier.send(meter_id, f"Unusual reading: {kwh} kWh at {at:%H:%M}")
            total += Decimal(str(kwh)) * self.prices.price_at(at)
        return total.quantize(Decimal("0.01"), ROUND_HALF_UP)


# ---- test doubles -------------------------------------------------------------
class StubPrices:
    """Stub: canned hourly prices instead of a call to the market."""

    CANNED = {14: Decimal("0.2850"), 18: Decimal("0.3410"), 23: Decimal("0.1910")}

    def price_at(self, at):
        return self.CANNED[at.hour]


class FakeReadings:
    """Fake: a working repository that keeps rows in a dict."""

    def __init__(self):
        self.rows = {}

    def add(self, meter_id, at, kwh):
        self.rows.setdefault(meter_id, []).append((at, kwh))

    def for_day(self, meter_id, day):
        return [(at, kwh) for at, kwh in self.rows.get(meter_id, []) if at.date() == day]


class SpyNotifier:
    """Spy: sends nothing, but remembers every call."""

    def __init__(self):
        self.sent = []

    def send(self, meter_id, message):
        self.sent.append((meter_id, message))


UNUSED_AUDIT_LOG = object()    # Dummy: the constructor needs it, these tests never use it


# ---- tests --------------------------------------------------------------------
def test_each_reading_is_priced_at_its_own_hour():
    readings = FakeReadings()
    readings.add("M-30007", datetime(2026, 10, 5, 14, 15), 1.0)
    readings.add("M-30007", datetime(2026, 10, 5, 23, 15), 1.5)
    readings.add("M-30007", datetime(2026, 10, 6, 0, 15), 2.0)    # next day
    daily_cost = DailyCost(StubPrices(), readings, SpyNotifier(), UNUSED_AUDIT_LOG)
    assert daily_cost.total("M-30007", date(2026, 10, 5)) == Decimal("0.57")


def test_customer_is_warned_about_a_spike():
    readings = FakeReadings()
    readings.add("M-30001", datetime(2026, 10, 5, 18, 0), 0.4)
    readings.add("M-30001", datetime(2026, 10, 5, 18, 15), 91.3)
    notifier = SpyNotifier()
    DailyCost(StubPrices(), readings, notifier, UNUSED_AUDIT_LOG).total("M-30001", date(2026, 10, 5))
    assert notifier.sent == [("M-30001", "Unusual reading: 91.3 kWh at 18:15")]
..                                                                       [100%]
2 passed in 0.16s

The first test expects €0.57: 1.0 kWh at €0.2850 plus 1.5 kWh at €0.1910 is €0.5715; the reading after midnight belongs to the next day. The prices are made up for the test, which is the point: the stub decides them, so the expected result can be worked out in advance.

Dummy

A dummy object is passed around only to fill a required parameter and is never actually used. UNUSED_AUDIT_LOG = object() is one: the constructor wants an audit log, but these tests never correct a price. If the code under test does use a dummy by mistake, the test fails loudly, because a bare object() has no methods. That is useful information too.

Stub

A stub provides canned answers to the calls made during the test and nothing else. StubPrices returns a fixed price per hour instead of asking the market. Stubs let you put the code under test in a situation that is hard to create for real: a negative market price on a sunny afternoon, a missing hour, a timeout. The test then checks the result of the code under test, not the stub.

Spy

A test spy is a stub that also records how it was called, so the test can inspect the calls afterward. SpyNotifier sends nothing and keeps a list of messages; the second test checks that list. Use a spy when the visible effect of the code is a call to something else, such as "the customer gets one warning", which has no return value to check.

Mock

A mock object is set up in advance with expectations about which calls it should receive, and it verifies those calls: the test fails if a call is missing, unexpected or has the wrong arguments. The difference with a spy is subtle and mostly in the style: with a spy you look at what happened afterward; with a mock, the expected interaction is the specification. Mocking libraries such as Python's unittest.mock and Java's Mockito create objects that can act as stub, spy or mock depending on how you use them, which is why the word "mock" is used so loosely.

Fake

A fake object has a real, working implementation, but takes a shortcut that makes it unsuitable for production. FakeReadings stores rows in a dictionary instead of PostgreSQL; an in-memory SQLite database, a fake SMTP server that keeps emails in a list, or a local file instead of cloud storage are other fakes. A fake behaves consistently (what you add, you can read back), so tests can use it naturally, and one good fake serves hundreds of tests. Its risk is that it drifts from the real thing; a few integration tests against the real database keep it honest.

Kind Answers calls Records calls Checks calls Cost page example
Dummy No No No The audit log in a pricing test
Stub Canned No No Fixed hourly prices
Spy Canned Yes The test does, afterward The notifier's list of messages
Mock Canned Yes Yes, against expectations "send must be called once with M-30001"
Fake Really computed Has real state No In-memory reading repository

Python's unittest.mock

unittest.mock is the standard-library module for creating test doubles on the fly. Its Mock class accepts any attribute access and any call, returns another mock, and records everything; you configure what it returns (return_value), what it raises or returns over successive calls (side_effect), and afterward ask how it was called (call_count, call_args, call_args_list, and assertion methods such as assert_called_once_with). MagicMock is the same with Python's special methods (len(), iteration, with) preconfigured. It works with pytest and with unittest alike:

from unittest.mock import Mock, call, create_autospec


class Notifier:
    def send(self, meter_id, message):
        """Send an SMS or email to the customer (needs the SMS gateway)."""


def warn_about_spikes(readings, notifier):
    for meter_id, kwh in readings:
        if kwh > 25:
            notifier.send(meter_id, f"Unusual reading: {kwh} kWh")


# A Mock accepts any call and records it
notifier = Mock()
warn_about_spikes([("M-30001", 0.4), ("M-30001", 91.3), ("M-30002", 30.0)], notifier)
print("calls:", notifier.send.call_count)
print(notifier.send.call_args_list == [
    call("M-30001", "Unusual reading: 91.3 kWh"),
    call("M-30002", "Unusual reading: 30.0 kWh"),
])
notifier.send.assert_called_with("M-30002", "Unusual reading: 30.0 kWh")
print("last call checked")

# return_value and side_effect give canned answers
prices = Mock()
prices.price_at.return_value = 0.224
print(prices.price_at("23:15"))
prices.price_at.side_effect = [0.31, 0.29, TimeoutError("price service")]
print(prices.price_at("07:00"), prices.price_at("08:00"))
try:
    prices.price_at("09:00")
except TimeoutError as error:
    print("TimeoutError:", error)

# A plain Mock also accepts a method that does not exist
notifier.sned("M-30001", "typo")
print("sned accepted")

# An autospec mock only accepts what the real class has
strict = create_autospec(Notifier, instance=True)
try:
    strict.sned("M-30001", "typo")
except AttributeError as error:
    print("AttributeError:", error)
try:
    strict.send("M-30001")
except TypeError as error:
    print("TypeError:", error)
calls: 2
True
last call checked
0.224
0.31 0.29
TimeoutError: price service
sned accepted
AttributeError: Mock object has no attribute 'sned'
TypeError: missing a required argument: 'message'

The last part is the most important. A plain Mock is happy with notifier.sned(...), a method that does not exist, so a typo in the code under test passes the test. create_autospec (or autospec=True when patching, below) builds the mock from the real class: unknown attributes raise AttributeError, and calls with the wrong arguments raise TypeError, just like the real object would. Prefer it whenever there is a real class to copy.

Patching: replace the name where it is looked up

When the code under test does not accept its dependency as a parameter, a test can temporarily replace it with unittest.mock.patch: inside the with block (or the decorated test), the given name points to a mock, and afterward the original comes back. The rule that trips everyone up: patch the name where the code under test looks it up, not where it was defined. Here cost_page.py does from prices import fetch_hourly_prices, which copies the name into the cost_page module at import time:

cat > prices.py <<'PY'
def fetch_hourly_prices(day):
    """Ask the market price service (network) for 24 hourly prices."""
    raise RuntimeError("network call made during a test")
PY

cat > cost_page.py <<'PY'
from prices import fetch_hourly_prices


def cheapest_hour(day):
    prices = fetch_hourly_prices(day)
    return prices.index(min(prices))
PY

cat > test_cost_page.py <<'PY'
from unittest.mock import patch

import cost_page

PRICES = [0.30] * 24
PRICES[3] = 0.12


def test_patch_where_it_is_defined():
    with patch("prices.fetch_hourly_prices", return_value=PRICES):
        assert cost_page.cheapest_hour("2026-10-07") == 3


def test_patch_where_it_is_looked_up():
    with patch("cost_page.fetch_hourly_prices", return_value=PRICES):
        assert cost_page.cheapest_hour("2026-10-07") == 3
PY

pytest -q --tb=short -p no:cacheprovider
F.                                                                       [100%]
=================================== FAILURES ===================================
________________________ test_patch_where_it_is_defined ________________________
test_cost_page.py:11: in test_patch_where_it_is_defined
    assert cost_page.cheapest_hour("2026-10-07") == 3
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
cost_page.py:5: in cheapest_hour
    prices = fetch_hourly_prices(day)
             ^^^^^^^^^^^^^^^^^^^^^^^^
prices.py:3: in fetch_hourly_prices
    raise RuntimeError("network call made during a test")
E   RuntimeError: network call made during a test
=========================== short test summary info ============================
FAILED test_cost_page.py::test_patch_where_it_is_defined - RuntimeError: netw...
1 failed, 1 passed in 0.17s

The first test replaced prices.fetch_hourly_prices, but cost_page still holds its own reference to the original function, so the real (here: deliberately failing) network call ran. The second test patched cost_page.fetch_hourly_prices, the name the code actually uses, and passed. Had cost_page.py written import prices and called prices.fetch_hourly_prices(day), patching prices.fetch_hourly_prices would have worked, because the lookup then happens in the prices module at call time.

Monkeypatching with pytest

Monkeypatching is replacing an attribute, function, class or environment variable at run time, from outside the code that uses it. In tests it must always be undone afterward, or one test changes the world for the next. pytest's built-in monkeypatch fixture does that for you: setattr, setenv, delenv and chdir are all reverted when the test ends, whether it passed or failed.

A classic use is the clock. "Is it night now?" depends on when the test runs, which makes a test that passes in the afternoon and fails at 23:30. Compare patching the clock with passing it in:

import os
from datetime import datetime


def is_night(at):
    return at.hour < 7 or at.hour >= 23


def current_tariff():
    """Hard to test: reads the real clock."""
    return "night" if is_night(datetime.now()) else "day"


def tariff_at(clock):
    """Easy to test: the caller passes in the clock."""
    return "night" if is_night(clock()) else "day"


def price_api_url():
    return os.environ.get("PRICE_API_URL", "https://api.kestrel.example/prices/v1")


def test_current_tariff_late_evening(monkeypatch):
    class FrozenDatetime(datetime):
        @classmethod
        def now(cls, tz=None):
            return cls(2026, 10, 5, 23, 15)

    monkeypatch.setattr("test_example.datetime", FrozenDatetime)
    assert current_tariff() == "night"


def test_tariff_with_an_injected_clock():
    assert tariff_at(lambda: datetime(2026, 10, 5, 23, 15)) == "night"


def test_price_api_from_the_environment(monkeypatch):
    monkeypatch.setenv("PRICE_API_URL", "http://127.0.0.1:8099/v1")
    assert price_api_url() == "http://127.0.0.1:8099/v1"


def test_default_price_api(monkeypatch):
    monkeypatch.delenv("PRICE_API_URL", raising=False)
    assert price_api_url() == "https://api.kestrel.example/prices/v1"
============================= test session starts ==============================
collecting ... collected 4 items

test_example.py::test_current_tariff_late_evening PASSED                 [ 25%]
test_example.py::test_tariff_with_an_injected_clock PASSED               [ 50%]
test_example.py::test_price_api_from_the_environment PASSED              [ 75%]
test_example.py::test_default_price_api PASSED                           [100%]

============================== 4 passed in 0.17s ===============================

Both clock tests pass, but look at the effort. The first one has to know that current_tariff uses the name datetime in this module, and replace that whole class with a subclass, because the built-in datetime.now cannot be patched directly. The second one just passes a function that returns a fixed time. That is dependency injection again, and it is the better design: when a test needs patching, the code is often telling you that a dependency is hidden. Monkeypatching is the right tool for things you cannot easily pass in, such as environment variables (the last two tests) or a third-party module's internals.

State or behavior: two schools

There are two ways to check that code did the right thing. State verification looks at the result: the returned total, the rows in the fake repository, the messages the spy collected. Behavior verification checks the interactions: the price service was asked for hour 23, the notifier was called once with these arguments. Fowler's "Mocks Aren't Stubs" describes the two styles:

  • Classicist (also "Detroit" or "Chicago" school): use real objects where possible, fakes and stubs where not, and verify state. Tests survive refactoring well, because they do not care how the result was reached.
  • Mockist ("London" school, after the London XP community where mock objects were invented; its best-known book is Growing Object-Oriented Software, Guided by Tests by Steve Freeman and Nat Pryce, 2009): mock every collaborator and verify the interactions. This drives the design of the interfaces between objects, but ties the tests to them.

Most teams mix the two: state verification by default, and a mock or spy where the interaction is the behavior, as with "send exactly one warning" or "never call the payment provider twice".

Over-mocking

Over-mocking is using so many mocks, or mocks so detailed, that tests mirror the implementation instead of checking behavior. It hurts in two opposite ways.

Tests that break for nothing

Bram Peeters rewords the warning to be friendlier. The customer still gets exactly one warning about the right meter and the right amount, but the test that pinned the exact text fails:

from unittest.mock import Mock, call


def warn_about_spikes(readings, notifier):
    for meter_id, kwh in readings:
        if kwh > 25:
            notifier.send(meter_id, f"Unusual reading of {kwh} kWh, please check your meter")


def test_customer_is_warned_once_about_a_spike():
    notifier = Mock()
    warn_about_spikes([("M-30001", 0.4), ("M-30001", 91.3)], notifier)
    assert notifier.send.call_args_list == [call("M-30001", "Unusual reading: 91.3 kWh")]


def test_customer_is_warned_once_about_a_spike_robust():
    notifier = Mock()
    warn_about_spikes([("M-30001", 0.4), ("M-30001", 91.3)], notifier)
    assert notifier.send.call_count == 1
    meter_id, message = notifier.send.call_args.args
    assert meter_id == "M-30001"
    assert "91.3 kWh" in message
F.                                                                       [100%]
=================================== FAILURES ===================================
__________________ test_customer_is_warned_once_about_a_spike __________________

    def test_customer_is_warned_once_about_a_spike():
        notifier = Mock()
        warn_about_spikes([("M-30001", 0.4), ("M-30001", 91.3)], notifier)
>       assert notifier.send.call_args_list == [call("M-30001", "Unusual reading: 91.3 kWh")]
E       AssertionError: assert [call('M-3000... your meter')] == [call('M-3000...g: 91.3 kWh')]
E
E         At index 0 diff: call('M-30001', 'Unusual reading of 91.3 kWh, please check your meter') != call('M-30001', 'Unusual reading: 91.3 kWh')
E         Use -v to get more diff

test_example.py:13: AssertionError
=========================== short test summary info ============================
FAILED test_example.py::test_customer_is_warned_once_about_a_spike - Assertio...
1 failed, 1 passed in 0.25s

The second test checks what matters (one warning, the right meter, the amount) and survives the rewording. Multiply the first style by a few hundred tests, each asserting every call with every argument, and every refactoring turns into an afternoon of updating tests that found nothing wrong. Teams then learn to update failing tests without reading them, and the suite stops protecting anything.

Mocks that lie

The other danger is a test that passes while the real thing is broken. A mock only knows what the test told it. In version 2 of the price client, price_at was renamed to hourly_price, but the cost code was not updated:

from unittest.mock import Mock, create_autospec


class PriceService:
    """The real client. Version 2 renamed price_at to hourly_price."""

    def hourly_price(self, day, hour):
        raise RuntimeError("calls the market price API")


def cost_at(prices, day, hour, kwh):
    return round(kwh * prices.price_at(day, hour), 2)    # still uses the old name


def test_with_a_plain_mock():
    prices = Mock()
    prices.price_at.return_value = 0.20
    assert cost_at(prices, "2026-10-07", 18, 1.5) == 0.30


def test_with_an_autospec_mock():
    prices = create_autospec(PriceService, instance=True)
    prices.price_at.return_value = 0.20
    assert cost_at(prices, "2026-10-07", 18, 1.5) == 0.30


for test in (test_with_a_plain_mock, test_with_an_autospec_mock):
    try:
        test()
        print(f"{test.__name__}: passed")
    except Exception as error:
        print(f"{test.__name__}: failed with {type(error).__name__}: {error}")
test_with_a_plain_mock: passed
test_with_an_autospec_mock: failed with AttributeError: Mock object has no attribute 'price_at'

(The two tests are called by hand here, so that the output shows both outcomes side by side; under pytest the second would be a normal failure.) The plain mock happily invents price_at, so the test is green while production would crash on the first dynamic-price customer. The autospec mock follows the real class and catches it. Autospec only helps when the class is in your code base, though. For a service on the other side of a network, such as meter-api or the market price API, a mock can only repeat your assumptions about its responses; contract tests check those assumptions against the real provider.

A rule of thumb from the mockist school itself is "don't mock what you don't own": instead of mocking a third-party client library directly, wrap it in a small class of your own (a PriceService with the one method you need), mock or fake that in unit tests, and test the wrapper against the real library or service in a few integration tests.

Choosing a test double

Situation Use
The dependency is fast and has no side effects (a price calculation, a parser) The real object; no double
You need specific answers from a slow or external service A stub
Many tests store and read data A fake, checked by a few integration tests
The call itself is the behavior (a warning, a payment) A spy or mock, checking only what matters
The code reads the clock, randomness or environment Pass it in; otherwise monkeypatch
A mock of a class from your own code create_autospec or autospec=True
The service is on the other side of a network A stub or mock server in unit tests, plus contract tests