Learning Goals
3 min- Explain why some dependencies can't be used in unit tests.
- Name the test-double family: mock, stub, fake, spy.
- Decide WHEN to mock — and when NOT to (the over-mocking trap).
- Recognise "seams" in code where a stand-in can be inserted.
Warm-Up · Untestable Dependencies
5 mindef weather_advice(city): temp = requests.get(f"https://api.weather.com/{city}").json()["temp"] return "hot!" if temp > 30 else "mild"
How do you unit-test this? You can't — it needs the internet, the API might be down, the temperature changes every minute, and you'd pay/rate-limit for every test run. The fix: replace the network call with a stand-in that returns a known temperature.
A unit test should test YOUR logic, not the network/clock/database. Mocking swaps those external dependencies for fakes you control, so the test is fast, offline, repeatable — and actually tests the thing you wrote (the "hot vs mild" decision), not the weather.
New Concept · Test Doubles & When to Use Them
14 minThe family of test doubles
stub returns canned answers ("always return temp=35")
mock a stub that ALSO records how it was called (so you can assert on it)
fake a working but simplified version (in-memory DB instead of real one)
spy wraps the real thing but records callsIn practice, Python's unittest.mock object does most of these — people just say "mock" for all of them. Lessons 22-23 cover the actual tools.
WHEN to mock
✅ network calls (APIs) — slow, flaky, costly, offline-unfriendly ✅ databases (sometimes) — slow setup; or use a real in-memory one ✅ the clock / dates — make "today" deterministic ✅ randomness — make rolls predictable ✅ sending email / payments — you DON'T want real side effects in a test! ✅ slow computations — replace with a fixed result
When NOT to mock — the over-mocking trap
❌ your own pure functions — just call them, they're fast & safe
❌ simple data structures — a real list/dict is better than a fake
❌ everything — a test that mocks all its dependencies
tests only the mocks, not real behaviourGolden rule: mock the boundary (the external world), test the logic (your code). If a test mocks so much that it only verifies the mocks were called, it proves nothing.
Seams — where you insert a double
# hard to mock — the dependency is created INSIDE def advice(): temp = WeatherApi().current() # tightly coupled # easy to mock — the dependency is PASSED IN (dependency injection) def advice(weather): temp = weather.current() # pass a fake 'weather' in tests
Code that accepts its dependencies as arguments is far easier to test — you just pass a fake. This is why testing improves design.
Worked Example · A Hand-Made Fake
12 minBefore the mock library, you can mock by hand — and it shows exactly what mocking is. Refactor the weather code to accept its dependency:
# weather.py — dependency injected, testable def weather_advice(weather_client, city): temp = weather_client.current_temp(city) return "hot!" if temp > 30 else "mild"
# test_weather.py — a hand-written fake client class FakeWeather: def __init__(self, temp): self._temp = temp def current_temp(self, city): return self._temp # canned answer, no network def test_hot_advice(): fake = FakeWeather(35) assert weather_advice(fake, "KL") == "hot!" def test_mild_advice(): fake = FakeWeather(22) assert weather_advice(fake, "Genting") == "mild" def test_boundary_30_is_mild(): assert weather_advice(FakeWeather(30), "X") == "mild" # > 30, so 30 is mild
$ pytest test_weather.py -v test_hot_advice PASSED test_mild_advice PASSED test_boundary_30_is_mild PASSED 3 passed in 0.01s ← no internet, instant, deterministic
Read the diff
The FakeWeather stand-in returns a known temperature, so we test the real logic — the hot/mild decision and its boundary at 30 — without touching the network. We also injected the client as an argument, which made the mock trivial. The next lessons use unittest.mock to do this without hand-writing the fake.
Try It Yourself
13 minList 4 functions you've written that touch the outside world (file, network, clock, random). For each, what would you replace in a test?
Refactor one tightly-coupled function to accept its dependency as an argument. Write a hand-made fake and test the logic.
Look at a test (yours or an example) that mocks heavily. Identify any mock that hides real logic. Could a real object be used instead?
Mini-Challenge · Make It Testable
8 minHere's a function that's impossible to unit-test. Refactor it to inject its dependencies, then write deterministic tests with hand-made fakes.
# untestable import random, datetime def daily_greeting(): hour = datetime.datetime.now().hour mood = random.choice(["cheerful", "calm"]) part = "morning" if hour < 12 else "afternoon" return f"Good {part}, feeling {mood}!"
Show one possible solution
# testable — inject the unpredictable bits def daily_greeting(hour, mood): part = "morning" if hour < 12 else "afternoon" return f"Good {part}, feeling {mood}!" def test_morning(): assert daily_greeting(9, "calm") == "Good morning, feeling calm!" def test_afternoon(): assert daily_greeting(15, "cheerful") == "Good afternoon, feeling cheerful!" def test_noon_boundary(): assert daily_greeting(12, "calm").startswith("Good afternoon")
By injecting hour and mood, the logic becomes a pure function — no mock library needed at all. Often the best "mocking" is restructuring so you don't need to mock.
Recap
3 minMock the boundary (network, DB, clock, random, side-effects), test the logic. Test doubles: stub (canned answers), mock (records calls), fake (simple working version), spy. Don't over-mock — a test that only verifies its mocks proves nothing. Inject dependencies to create seams; sometimes restructuring removes the need to mock entirely. Next: the real Mock and MagicMock.
Vocabulary Card
- test double
- Any stand-in for a real dependency in a test.
- stub / mock / fake
- Canned answers / records calls / simplified working version.
- dependency injection
- Passing dependencies in as arguments so they can be swapped in tests.
- over-mocking
- Mocking so much that the test no longer exercises real behaviour.
Homework
4 minTake a function of yours that depends on the outside world (file, API, clock, random). Refactor it to inject the dependency, write a hand-made fake, and test the pure logic deterministically. Write one sentence on whether mocking or restructuring was the better fix.
Follow the daily_greeting refactor. Often the cleanest answer is "I didn't need a mock — I made the function pure by injecting the value."