API Testing & Quality Engineering
ConceptIntermediate 25 min· Automated tests · 9 of 19

Mocks, stubs and test doubles

Learn when and how to replace slow, costly or unreliable dependencies in tests. Understand stubs, mocks, spies and fakes, write them in Python with monkeypatch and unittest.mock, and avoid the classic patching and over-mocking mistakes.

Imagine you are testing the order code of the coffee shop. When an order is placed, the code sends an SMS to the customer through an outside SMS service. If your test calls the real service, it is slow, it can cost money, it sends real messages to real phones, and it fails when the internet is down. None of those are bugs in your code.

The fix is a test double: a stand-in object that replaces the real dependency during a test, like a stunt double in a film. This lesson explains the kinds of doubles, when to use each, and how to write them in Python (and a short look at JavaScript).

The code we will test

Two small files. sms.py talks to an outside provider (the URL is made up). orders.py has the business logic: work out the total, refuse orders while the shop is closed (open 8 am to 10 pm), and send an SMS. Notice that place_order takes the current time as an argument instead of reading the clock itself. That makes it easy to test, and we will come back to why.

sms.py
import httpx


def send_sms(phone: str, text: str) -> bool:
    """Calls a (made-up) SMS provider. Slow, costs money, can fail."""
    response = httpx.post(
        "https://sms.example.com/send",
        json={"to": phone, "text": text},
        timeout=5,
    )
    return response.status_code == 200

The slow, costly dependency. We never want a unit test to run this.

orders.py
from datetime import datetime

import sms

PRICES = {"espresso": 120, "croissant": 150}


def order_total(items: list[dict]) -> int:
    return sum(PRICES[i["item_id"]] * i["qty"] for i in items)


def place_order(phone: str, items: list[dict], now: datetime) -> dict:
    if now.hour < 8 or now.hour >= 22:
        raise ValueError("Shop is closed (open 8 am to 10 pm)")
    total = order_total(items)
    notified = sms.send_sms(phone, f"Order received. Total Rs {total}.")
    return {"total": total, "notified": notified}

The logic we want to test. It calls sms.send_sms through the sms module.

The five kinds of test double

People use the words mock, stub and fake loosely, and in daily work that is fine. But knowing the differences helps you choose, and interviewers like to ask.

Test doubles at a glance

DoubleWhat it doesUse it when
DummyFills a required argument and is never usedA function needs a parameter your test does not care about
StubReturns canned answersYou need the dependency to return a specific value, such as success or failure
SpyWorks (or is a stub) and records how it was calledYou want to check that something was called, and with what
MockA double with expectations built in: it fails the test if it was not called as expectedYou want to check the interaction itself, such as "exactly one SMS was sent"
FakeA simple but working replacement, such as an in-memory databaseThe real thing is too heavy, but the logic is non-trivial

In Python, unittest.mock.Mock is a spy and a mock in one: it records every call, and you assert on the record afterwards with methods like assert_called_once_with. That is why most Python developers just say "mock" for everything.

Stubs: control what the dependency returns

A stub answers questions. In the first test we replace sms.send_sms with a tiny function that always returns True. In the second, one that returns False, so we can check how our code behaves when the SMS provider fails, something that is hard to trigger with the real service. pytest's monkeypatch fixture does the replacing and automatically puts the original back when the test ends.

test_orders.py
from datetime import datetime
from unittest.mock import Mock

import pytest

import orders

ITEMS = [{"item_id": "espresso", "qty": 2}]
NOON = datetime(2026, 11, 2, 12, 0)
MIDNIGHT = datetime(2026, 11, 2, 0, 30)


def test_stub_returns_canned_answer(monkeypatch):
    monkeypatch.setattr(orders.sms, "send_sms", lambda phone, text: True)

    result = orders.place_order("9876543210", ITEMS, NOON)

    assert result == {"total": 240, "notified": True}


def test_stub_can_simulate_failure(monkeypatch):
    monkeypatch.setattr(orders.sms, "send_sms", lambda phone, text: False)

    result = orders.place_order("9876543210", ITEMS, NOON)

    assert result["notified"] is False


def test_mock_checks_the_conversation(monkeypatch):
    fake_send = Mock(return_value=True)
    monkeypatch.setattr(orders.sms, "send_sms", fake_send)

    orders.place_order("9876543210", ITEMS, NOON)

    fake_send.assert_called_once_with("9876543210", "Order received. Total Rs 240.")


def test_closed_shop_sends_no_sms(monkeypatch):
    fake_send = Mock()
    monkeypatch.setattr(orders.sms, "send_sms", fake_send)

    with pytest.raises(ValueError, match="closed"):
        orders.place_order("9876543210", ITEMS, MIDNIGHT)

    fake_send.assert_not_called()


class FakeSmsGateway:
    """A fake: a simple working replacement that keeps what it was sent."""

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

    def send_sms(self, phone, text):
        self.sent.append((phone, text))
        return True


def test_fake_keeps_messages(monkeypatch):
    gateway = FakeSmsGateway()
    monkeypatch.setattr(orders, "sms", gateway)

    orders.place_order("9876543210", ITEMS, NOON)

    assert gateway.sent == [("9876543210", "Order received. Total Rs 240.")]

Five tests: two stubs, a mock that checks the call, a mock that checks nothing was sent, and a fake. Run with pytest -v.

Expected output (install first with `pip install pytest httpx`)
test_orders.py::test_stub_returns_canned_answer PASSED
test_orders.py::test_stub_can_simulate_failure PASSED
test_orders.py::test_mock_checks_the_conversation PASSED
test_orders.py::test_closed_shop_sends_no_sms PASSED
test_orders.py::test_fake_keeps_messages PASSED

5 passed in 0.2s

Mocks: check the conversation

A stub tests what comes back. A mock tests what was done. test_mock_checks_the_conversation asserts that the SMS function was called exactly once with the right phone number and message. test_closed_shop_sends_no_sms uses assert_not_called() to prove that a rejected order did not send a message to the customer.

These checks matter when the "result" of the code is a side effect (an email, an SMS, a payment) that nobody can see in a return value.

Fakes: a small working replacement

FakeSmsGateway has real behaviour: it stores every message it receives in a list, so the test can look at gateway.sent afterwards. A fake is more work to write than a stub, but you write it once and reuse it in many tests. Typical fakes: an in-memory repository instead of a database, a fake payment gateway that approves everything except one magic card number, a fake clock.

To see a fake in a bigger setting, the next lesson *Integration tests with a real database* uses a real (temporary) database instead, which is the opposite choice. Choosing between them is the real skill.

Patch where it is used, not where it is defined

The most common mocking bug in Python: your patch seems to do nothing and the real function still runs. It happens because of how imports bind names.

If orders.py says import sms and calls sms.send_sms(...), patching orders.sms.send_sms (or sms.send_sms) works, because the lookup happens at call time through the module.

If orders.py says from sms import send_sms, then orders has its own name send_sms, copied at import time. Patching sms.send_sms changes the original, but orders.send_sms still points at the real function. You must patch orders.send_sms instead. The rule: patch the name in the module that uses it.

Which name to patch
# orders.py uses:  from sms import send_sms
monkeypatch.setattr("orders.send_sms", lambda phone, text: True)   # works
monkeypatch.setattr("sms.send_sms", lambda phone, text: True)      # silently does nothing for orders.py

The string form of monkeypatch.setattr takes a dotted path to the name you want to replace.

Mocking the HTTP call itself

The tests above replace send_sms completely, so the code inside sms.py is never run. To test send_sms itself, replace only the network layer. httpx has a built-in MockTransport that answers requests with a function you write, so no network is used:

test_provider_http.py
import httpx

import sms


def test_send_sms_with_mock_transport(monkeypatch):
    def handler(request: httpx.Request) -> httpx.Response:
        assert request.url.path == "/send"
        return httpx.Response(200, json={"ok": True})

    def post_via_mock(url, **kwargs):
        with httpx.Client(transport=httpx.MockTransport(handler)) as client:
            return client.post(url, **kwargs)

    monkeypatch.setattr(sms.httpx, "post", post_via_mock)
    assert sms.send_sms("9876543210", "hi") is True

The handler can also check the request (here, the path) and return any status you want, such as 500, to test error handling.

Do not over-mock

Mocks make tests fast and isolated, but they have a cost. A mock only knows what you told it. If the real SMS provider changes its API, your mocked tests still pass while production breaks. And if you mock everything, the test checks your mocks, not your code.

A useful rule of thumb:

What to replace and what to keep real

DependencyUsual choiceWhy
Paid or external service (SMS, payments, email)Mock, stub or fakeCost, speed, no side effects, can simulate failures
Current time, random numbersPass them in, or fake themMakes tests repeatable
Your own databaseReal test database (see next lesson)Queries, constraints and transactions are bugs waiting to happen
Your own pure functionsNever mockThey are fast and deterministic already
Another team's serviceMock plus a contract testThe contract test notices when the real one changes

Brittle tests

Over-specified mocks make tests brittle. If a test asserts the exact order of five internal calls, any harmless refactor breaks it. Assert on outcomes and the important interactions only.

Prefer injection when you can

Passing the dependency in (here, now as an argument) is called dependency injection. It is often simpler than patching. In place_order we pass the time, so a test can use noon or half past midnight directly and no clock mocking is needed.

The same idea in JavaScript

In Vitest or Jest the building blocks have different names: vi.fn() (or jest.fn()) creates a mock function that records calls, .mockResolvedValue(...) sets a canned async answer (a stub), and vi.spyOn(obj, "method") wraps a real method so you can watch it. Fake timers (vi.useFakeTimers()) control the clock.

orders.test.js
import { test, expect, vi } from "vitest";

export async function placeOrder(sendSms, phone, total) {
  const notified = await sendSms(phone, `Order received. Total Rs ${total}.`);
  return { total, notified };
}

test("sends one SMS with the total", async () => {
  const sendSms = vi.fn().mockResolvedValue(true);

  const result = await placeOrder(sendSms, "9876543210", 240);

  expect(result).toEqual({ total: 240, notified: true });
  expect(sendSms).toHaveBeenCalledTimes(1);
  expect(sendSms).toHaveBeenCalledWith("9876543210", "Order received. Total Rs 240.");
});

Here the dependency is passed in as a function argument, so no patching is needed. For Jest, import nothing and use jest.fn() instead of vi.fn().

Using AI to help

Prompt · Find what to mock in a function
You are a senior test engineer.

Here is a function and the modules it imports:
[paste code]

1. List every dependency that makes it slow, non-deterministic or risky to test (network, database, clock, random, file system, paid services).
2. For each one, recommend stub, mock, fake or real, with a one-line reason.
3. Write pytest tests using monkeypatch and unittest.mock. Show exactly which name to patch and why that is the right place.

What to expect: Check the patch targets yourself: AI often patches the module that defines a function instead of the module that uses it. Run the tests, then temporarily break the code and confirm a test fails.

How this comes up in interviews

Typical questions: "What is the difference between a mock and a stub?" (stub returns canned data, mock verifies interactions), "When would you not use a mock?", "Why did my patch not work?" (patching the wrong name) and "What is the risk of mocking?" (tests pass while the real integration is broken). A good answer gives one example of each and ends with: pair mocks with a few integration or contract tests.

Try it: Add a failure-handling rule

Change place_order so that when the SMS fails (send_sms returns False) the result has "notified": False and a new key "warning": "SMS not sent". Write the test first using a stub that returns False, watch it fail, then change the code until it passes.

Try it: Classify these

For each case, say whether you would use a stub, mock, fake or the real thing: (a) a test that checks a welcome email was sent once; (b) a repository that stores orders, used in 40 tests; (c) a function that adds two prices; (d) a payment gateway that must return "declined" in one test.

Key takeaways

  1. A test double replaces a real dependency so tests are fast, repeatable and free of side effects.
  2. Stubs return canned answers; mocks verify how they were called; fakes are simple working replacements; spies record calls.
  3. In pytest, monkeypatch swaps things temporarily and restores them; unittest.mock.Mock records calls.
  4. Patch the name where it is used, not where it is defined.
  5. Passing dependencies in (such as the current time) is often cleaner than patching.
  6. Do not over-mock: keep your own pure code and your own database real, and add contract or integration tests for what you mock.

Check yourself

Finishing the quiz marks the lesson as done.

1. A test needs the SMS provider to report failure so the code's error handling can be checked. Which double fits best?

2. What is the main difference between a mock and a stub?

3. orders.py has from sms import send_sms. Your test patches sms.send_sms but the real function still runs. Why?

4. Which dependency is usually best left real in a test of order saving?

5. What is the biggest risk of heavy mocking?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free