Command Palette

Search for a command to run...

Hectal
PHASE 11Intermediate ~31 min· topic 4 of 8

Topic 11.4

Mocks, Fakes and Patching

In one line

A mock or fake is a stand-in for something slow, unsafe or unpredictable (the network, the clock, a database, an AI model), so a test checks only your own code. unittest.mock provides Mock, MagicMock and patch for this.

Think of it like this

A flight simulator. Pilots practise emergencies in a simulator because crashing a real plane is expensive and dangerous. The simulator answers like a plane would, so the pilot's skill is tested without the risk. A test double is the simulator for a piece of code: it looks like the real thing from the outside, but you control what it does.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Test double
Any stand-in used in a test instead of the real thing.
Stub
A double that returns fixed answers.
Fake
A small but working replacement, such as an in-memory store.
Mock
A double that records how it was used so a test can check the calls.
`return_value` / `side_effect`
What a mock gives back, and how to make it raise errors or return different values per call.
`patch`
Temporarily replaces a name with a mock and restores it afterwards.
Dependency injection
Passing the things a function needs in as parameters, so tests can pass doubles.
Autospec
A mock that copies the real object's methods and signatures and rejects wrong calls.
Boundary
The place where your program talks to the outside world: network, disk, clock, other services.

Step by step

01The problem: code that talks to the world

This function is hard to test because it creates its own client and calls the real service. A test would hit the network, depend on today's answer and cost money per run. (make_real_client is a made-up name here, standing for "build the real thing".)

summary.pywhole filepython
def summarise(text):
    client = make_real_client()          # needs an API key and the internet
    reply = client.complete(f"Summarise in one sentence: {text}")
    return reply.strip()

02Pass the dependency in

Change one thing: take the client as a parameter. The production code passes the real client, and a test passes a fake. The function body is the same, and now it can be tested anywhere.

summary.pywhole filepython
class Summariser:
    def __init__(self, client):
        self.client = client

    def summarise(self, text):
        prompt = f"Summarise in one sentence: {text}"
        reply = self.client.complete(prompt)
        return reply.strip()

03Mock basics: return_value, side_effect, calls

A Mock can stand in for the client. Tell it what to return; ask it what it received. assert_called_once_with fails with a message that says what really happened, which is exactly what you need when debugging.

main.pywhole filepython
from unittest.mock import Mock

client = Mock()
client.complete.return_value = "Short."

print(client.complete("Hello"))           # Short.
client.complete.assert_called_once_with("Hello")

04Make the double fail on purpose

side_effect with an exception makes the call raise. With a list, the mock returns the items one by one (and raises any item that is an exception). That lets you test a retry loop: first call fails, second succeeds.

This is the biggest win of doubles: errors such as timeouts, rate limits and malformed replies that are rare and unpredictable in real life become one line in a test.

main.pywhole filepython
from unittest.mock import Mock

flaky = Mock(side_effect=[ConnectionError("down"), "ok"])
for attempt in (1, 2):
    try:
        print("attempt", attempt, "->", flaky())
    except ConnectionError as error:
        print("attempt", attempt, "-> error:", error)

05patch: patch where the name is looked up

Sometimes you cannot pass the dependency in (old code, a library call). Then use patch. The trap is the target string. If report.py says from rates import fetch_rate, the function lives in rates but the name the code calls is report.fetch_rate. Patch that one.

A way to remember: patch the name in the file that uses it.

patch: patch where the name is looked updiagram
Rendering diagram…

06Fake or mock? Pick the simplest that works

A hand-written fake is just a small class with the same method. It is easy to read and keeps working when you change how you assert. A Mock is faster to set up and records calls. For most model clients, a tiny fake that stores the prompts it received (fake.prompts) and returns a canned answer is clearer than a mock with many settings.

main.pywhole filepython
class FakeLLM:
    def __init__(self, answer):
        self.answer = answer
        self.prompts = []

    def complete(self, prompt):
        self.prompts.append(prompt)
        return self.answer

07Autospec: mocks that match the real thing

A plain Mock accepts a call with a misspelled keyword and never complains, so your test can pass while the real call would crash. create_autospec(Client, instance=True) builds a mock that copies the real class's methods and signatures. A wrong keyword or a wrong method name now fails the test immediately.

08Over-mocking: when the test proves nothing

If every function your code calls is mocked, the test only checks that your code calls the mocks in the order you wrote. It can pass with a completely broken program. Signs of over-mocking: the test breaks on a refactor that changes no behaviour; the assertions list internal calls; the mock returns exactly what you wish the real thing returned.

Rule of thumb: mock the outside world, use real objects for your own code, and keep a few integration tests that touch the real thing (a test API key, a local database) to prove the double matches reality.

Try it yourself

  1. 1

    Force an error path

    In the third example add a JunkLLM whose complete returns "" (an empty string). Call Summariser(JunkLLM()).summarise("x") and look at the result. Should the code treat an empty reply as an error? Decide, then change summarise and add a check.

  2. 2

    Make the patch work the other way

    In the patch example, change the text written to report.py so it says import rates and calls rates.fetch_rate(). Now patch("rates.fetch_rate", ...) works, because the code looks the name up in the rates module each time it runs. Explain why this differs from from rates import fetch_rate.

  3. 3

    Catch a typo with autospec

    In the autospec example, call strict.complete("hi", max_tokens=5, temperature=0). Predict the error, then run it.

Code & diagrams

Mock: return_value, side_effect, call records New tab

Add a third call to `sender.send` and watch `call_count` change. Then fix the last assertion so it passes.

Sign in to run this example in your browser.

Expected output

call count: 2
last call: call('sara@example.com', 'Welcome!')
all calls: [call('amit@example.com', 'Welcome!'), call('sara@example.com', 'Welcome!')]
rate: 83.5
attempt 1 -> error: down
attempt 2 -> ok
len: 3
assertion failed: Expected 'send' to be called once. Called 2 times.
patch: the wrong place and the right place New tab

report.py copied the name fetch_rate with `from rates import fetch_rate`. Try patching `rates.fetch_rate` in the second block and see what happens.

Sign in to run this example in your browser.

Expected output

patching rates.fetch_rate did not help: no network in tests
patched report.fetch_rate -> 10.0
after the with block: no network in tests
A fake and a mock stand in for an LLM client New tab

No network and no API key: the Summariser only needs an object with a `.complete(prompt)` method. Change the fake's answer and see the result change.

Sign in to run this example in your browser.

Expected output

fake result: 'Tests protect your code.'
fake saw: ['Summarise in one sentence: Long essay about tests']
mock result: Short.
prompt check passed
error path: model did not answer
Autospec rejects calls the real object would reject New tab
Sign in to run this example in your browser.

Expected output

plain Mock accepted a wrong call: call('hi', max_token=5)
autospec, right call: ok
autospec, wrong keyword -> TypeError: got an unexpected keyword argument 'max_token'
autospec, wrong method -> AttributeError: Mock object has no attribute 'complet'
Production code and test code use different clientsdiagram
Rendering diagram…

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

A Mock where a number was needed

Create the Mock but forget to set return_value.

main.pywhole filepython
from unittest.mock import Mock


def in_dollars(rupees, client):
    return round(rupees / client.rate("USD"), 2)


client = Mock()                 # forgot: client.rate.return_value = 80.0
print(in_dollars(800, client))
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 9, in <module>
print(in_dollars(800, client))
~~~~~~~~~~^^^^^^^^^^^^^^
File "main.py", line 5, in in_dollars
return round(rupees / client.rate("USD"), 2)
~~~~~~~^~~~~~~~~~~~~~~~~~~~
TypeError: unsupported operand type(s) for /: 'int' and 'Mock'

Myth vs fact

Myth

Mocking everything makes tests more isolated and therefore better.

Fact

Isolation helps only at the boundary. Mock your own code and the test just checks that your code calls itself, while real bugs pass through.

Myth

patch("module.function") patches the function everywhere.

Fact

It replaces one name in one module. Another module that already imported the function holds its own reference and is not affected.

Myth

If a mocked test passes, the real service works.

Fact

A mock only returns what you tell it. Keep a few integration tests against the real thing, or contract tests, to prove your assumptions about it.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    A Mock creates child mocks on the fly: mock.anything.you.like just works. That is why a typo in a real call passes silently. Since Python 3.5 a Mock does raise AttributeError for unknown attribute names that start with assert or assret (3.12 added a few more misspellings such as asert), so a misspelt assert_called_onec() fails loudly. The trap that remains is a name that does not start with assert at all: mock.called_once_with("x") (the assert_ forgotten) just creates a child mock, is never checked, and the test passes whatever happened. autospec closes that gap, because it only allows attributes the real object has.

  • ▸

    Mock(spec=Client) limits attribute names to those of Client; create_autospec(Client, instance=True) (or patch("mod.Client", autospec=True)) also checks signatures, and spec_set= additionally blocks setting new attributes. patch of an async def function creates an AsyncMock automatically (3.8+), whose calls you check with assert_awaited_once_with. A side_effect list that runs out raises StopIteration, so give it one more item than you expect to need.

  • ▸

    monkeypatch or mock.patch? Both swap a name for the length of a test. monkeypatch (pytest) only substitutes: you pass the replacement yourself (a fake function or object) and it is undone at teardown, which suits environment variables, sys.path and replacing a function with a tiny fake. mock.patch creates a MagicMock that records calls, so use it when the call itself is what you assert. Either way the target rule is the same: patch where the name is looked up.

  • ▸

    patch also works as a decorator, which injects the mock as an extra argument. With several decorators, the bottom one supplies the first argument, a frequent source of swapped mocks. A with block or a pytest fixture using monkeypatch is often easier to read.

  • ▸

    For the clock, prefer injecting a time source over patching time.time or datetime. Patching built-ins affects every library in the process. Libraries such as time-machine and freezegun exist for the cases where injection is not possible.

  • ▸

    In AI applications the most valuable double records requests. A fake model client that stores each prompt lets you assert that a system prompt, retrieved documents and user input were assembled correctly. A second layer of tests with a cheap real model, run less often, checks the actual quality.

Remember this

  1. 1

    Why bother? A good test is fast, offline, free and repeatable. Calling a real API (a service your program talks to over the internet), reading the real clock, sending a real email or asking a real language model breaks all four. It is slow, needs internet, may cost money, and gives a different answer next time.

  2. 2

    So at the boundary of your program (the place where it talks to the outside world) you swap the real thing for a double.

  3. 3

    There are a few kinds, and the names matter. A stub returns a fixed answer. A fake is a small, honest, working replacement, such as a class that keeps data in a dictionary instead of a database. A mock also records how it was called, so the test can check that the right call happened. Python's unittest.mock.Mock can play all three roles.

  4. 4

    A Mock object accepts any call and any attribute and remembers them: sender.send("a@b.c", "hi") works even though sender is empty. Set return_value to choose what a call gives back. Set side_effect to raise an error (side_effect=ConnectionError("down")) or to return a different value on each call (side_effect=[1, 2]).

  5. 5

    Afterwards, ask what happened: call_count, call_args, assert_called_once_with(...). MagicMock is a Mock that also supports magic methods such as len() and in (Topic 8.7).

  6. 6

    patch("module.name") replaces a name with a Mock while a with block (Topic 7.4) or decorated test runs, and puts the original back after.

  7. 7

    The golden rule: patch where the name is looked up, not where it was defined. If report.py does from rates import fetch_rate, then report has its own copy of the name fetch_rate (Topic 6.1), so you must patch report.fetch_rate. Patching rates.fetch_rate changes a different name and the code under test never notices. The runnable example below shows both.

  8. 8

    The best way to make code testable is to pass the dependency in as a parameter instead of reaching for a global name. This is called dependency injection (see Topic 8.11). Then the test just passes a fake and patch is not needed at all.

  9. 9

    For the clock, accept a now function or a date parameter. For web requests, pass the client (the httpx library, Topic 15.1, even ships httpx.MockTransport for this). For an AI model, pass the client object.

  10. 10

    Testing code that calls a language model follows the same recipe. Your code calls something like client.complete(prompt). In tests you hand it a fake client with the same method. The fake returns a canned reply (valid JSON, broken JSON, an empty string) or raises a timeout.

  11. 11

    Now you can check your prompt builder, your parser, your retry loop and your error handling in milliseconds, for free, with the same result every time. You can also force on demand the failures that a real model rarely produces. No real network call happens.

  12. 12

    Mocks have a classic failure: over-mocking. If you mock your own internal functions and assert that they were called in order, the test restates the implementation. It passes while the real code is broken, and it breaks on every harmless refactor. Mock only at the boundary, prefer a simple fake or the real object when cheap, and use create_autospec (checks method names and signatures) or spec= (checks attribute names only) so a mock refuses calls the real object would not accept.

Explain it without notes

01

What is the difference between a stub, a fake and a mock?

02

Where should you patch: where a name is defined or where it is used? Explain with an example.

03

How do you test a function that calls an AI model without calling the model?

04

What is over-mocking and how do you avoid it?

05

What does create_autospec give you that a plain Mock does not?

Practice

01

Write a FakeClock class with a now() method that returns a time you set, and a function greeting(clock) that returns "Good morning" before 12 and "Good afternoon" otherwise. Test both cases.

02

Use Mock(side_effect=[...]) to test a fetch_with_retry(client) function that retries once if the first call raises ConnectionError.

03

Use patch.object to replace a method on a class for one with block and show that the original returns afterwards.

Trade-offs

  • ↔

    Doubles make tests fast and stable, but they can drift from reality. A fake that behaves differently from the real service gives false confidence. Balance them with a small number of tests against the real thing.

  • ↔

    **Dependency injection versus patch.** Passing dependencies in needs a slightly different design but gives simple tests. patch works on code you cannot change, but ties tests to import details.

  • ↔

    Fakes versus mocks. Fakes read like real code and survive refactors; mocks are quick and good at asserting that something was called. Use a mock when the call itself is the behaviour (send the email), a fake when the result matters.

Done when you can

  • Done when you can explain stub, fake and mock in your own words.

  • Done when you can configure a Mock with return_value and side_effect and check calls.

  • Done when you can say where to patch and demonstrate the from x import y trap.

  • Done when you can test a function that uses an AI client by passing a fake with the same method.

  • Done when you can use create_autospec and explain what it catches.

  • Done when you can spot over-mocking in a test.