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".)
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.
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.
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.
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.
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.
class FakeLLM:
def __init__(self, answer):
self.answer = answer
self.prompts = []
def complete(self, prompt):
self.prompts.append(prompt)
return self.answer07Autospec: 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
Force an error path
In the third example add a
JunkLLMwhosecompletereturns""(an empty string). CallSummariser(JunkLLM()).summarise("x")and look at the result. Should the code treat an empty reply as an error? Decide, then changesummariseand add a check. - 2
Make the patch work the other way
In the patch example, change the text written to
report.pyso it saysimport ratesand callsrates.fetch_rate(). Nowpatch("rates.fetch_rate", ...)works, because the code looks the name up in theratesmodule each time it runs. Explain why this differs fromfrom rates import fetch_rate. - 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
Add a third call to `sender.send` and watch `call_count` change. Then fix the last assertion so it passes.
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.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.
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 testsNo 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.
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 answerExpected 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'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.
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))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
Mockcreates child mocks on the fly:mock.anything.you.likejust works. That is why a typo in a real call passes silently. Since Python 3.5 a Mock does raiseAttributeErrorfor unknown attribute names that start withassertorassret(3.12 added a few more misspellings such asasert), so a misspeltassert_called_onec()fails loudly. The trap that remains is a name that does not start withassertat all:mock.called_once_with("x")(theassert_forgotten) just creates a child mock, is never checked, and the test passes whatever happened.autospeccloses that gap, because it only allows attributes the real object has. - ▸
Mock(spec=Client)limits attribute names to those ofClient;create_autospec(Client, instance=True)(orpatch("mod.Client", autospec=True)) also checks signatures, andspec_set=additionally blocks setting new attributes.patchof anasync deffunction creates anAsyncMockautomatically (3.8+), whose calls you check withassert_awaited_once_with. Aside_effectlist that runs out raisesStopIteration, so give it one more item than you expect to need. - ▸
monkeypatchormock.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.pathand replacing a function with a tiny fake.mock.patchcreates aMagicMockthat 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. - ▸
patchalso 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. Awithblock or a pytest fixture usingmonkeypatchis often easier to read. - ▸
For the clock, prefer injecting a time source over patching
time.timeordatetime. Patching built-ins affects every library in the process. Libraries such astime-machineandfreezegunexist 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
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
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
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.Mockcan play all three roles. - 4
A
Mockobject accepts any call and any attribute and remembers them:sender.send("a@b.c", "hi")works even thoughsenderis empty. Setreturn_valueto choose what a call gives back. Setside_effectto raise an error (side_effect=ConnectionError("down")) or to return a different value on each call (side_effect=[1, 2]). - 5
Afterwards, ask what happened:
call_count,call_args,assert_called_once_with(...).MagicMockis aMockthat also supports magic methods such aslen()andin(Topic 8.7). - 6
patch("module.name")replaces a name with aMockwhile awithblock (Topic 7.4) or decorated test runs, and puts the original back after. - 7
The golden rule: patch where the name is looked up, not where it was defined. If
report.pydoesfrom rates import fetch_rate, thenreporthas its own copy of the namefetch_rate(Topic 6.1), so you must patchreport.fetch_rate. Patchingrates.fetch_ratechanges a different name and the code under test never notices. The runnable example below shows both. - 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
patchis not needed at all. - 9
For the clock, accept a
nowfunction or adateparameter. For web requests, pass the client (thehttpxlibrary, Topic 15.1, even shipshttpx.MockTransportfor this). For an AI model, pass the client object. - 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
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
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) orspec=(checks attribute names only) so a mock refuses calls the real object would not accept.
Explain it without notes
What is the difference between a stub, a fake and a mock?
Where should you patch: where a name is defined or where it is used? Explain with an example.
How do you test a function that calls an AI model without calling the model?
What is over-mocking and how do you avoid it?
What does create_autospec give you that a plain Mock does not?
Practice
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.
Use Mock(side_effect=[...]) to test a fetch_with_retry(client) function that retries once if the first call raises ConnectionError.
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.patchworks 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
Mockwithreturn_valueandside_effectand check calls.Done when you can say where to patch and demonstrate the
from x import ytrap.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_autospecand explain what it catches.Done when you can spot over-mocking in a test.