Fakes and dependency injection · Software Testing
How do you test code that sends emails, charges cards or depends on the time?
Course contentsWhy you need it
Real emails, real payments and the real clock make tests slow, flaky and sometimes expensive: you don't want every test run to charge a card. The fix is a design habit: let the code receive these things instead of reaching for them. Then a test can hand in a harmless stand-in, and the code is easier to change too.
The intuition
Hard to test:
def shop_status():
hour = datetime.datetime.now().hour # reaches out to the real clockEasy to test, because the time is passed in:
def shop_status(now):
hour = now.hourThat's dependency injection: give a function what it depends on (the time, a mailer, a database, a payment client) as arguments.
In tests you pass a test double instead of the real thing:
| Double | What it does |
|---|---|
| stub | returns fixed answers ("the time is 09:00") |
| fake | a small working version (an in-memory database) |
| mock / spy | records how it was called, so the test can check it |
Python's unittest.mock can build these for you, but a ten-line class is often clearer.
Definition
Dependency injection
Passing a function or object the things it depends on, instead of letting it create or fetch them itself.
Test double
A stand-in for a real dependency in tests: a stub, fake or mock.
Examples
Pass the time in
Try it yourselfWith now as a parameter, any moment can be tested.
import datetime
def shop_status(now):
if 9 <= now.hour < 18:
return "open"
return "closed"
print(shop_status(datetime.datetime(2026, 1, 5, 8, 59)))
print(shop_status(datetime.datetime(2026, 1, 5, 9, 0)))
print(shop_status(datetime.datetime(2026, 1, 5, 18, 0)))closed open closed
- L3The function no longer looks at the real clock.
- L8A test can pick exactly the boundary minutes: 08:59, 09:00, 18:00.
A fake mailer
Try it yourselfThe fake records what would have been sent, so the test can check it.
class FakeMailer:
def __init__(self):
self.sent = []
def send(self, to, subject):
self.sent.append((to, subject))
def remind_overdue(invoices, today, mailer):
for inv in invoices:
if inv["due"] < today and not inv["paid"]:
mailer.send(inv["email"], f"Invoice {inv['id']} is overdue")
invoices = [
{"id": 1, "email": "a@example.com", "due": "2026-03-01", "paid": False},
{"id": 2, "email": "b@example.com", "due": "2026-03-01", "paid": True},
{"id": 3, "email": "c@example.com", "due": "2026-04-01", "paid": False},
]
mailer = FakeMailer()
remind_overdue(invoices, "2026-03-10", mailer)
print(mailer.sent)[('a@example.com', 'Invoice 1 is overdue')]
- L1Same send() as the real mailer, but it only remembers.
- L8The code gets the mailer and today's date as arguments.
- L10ISO dates (YYYY-MM-DD) compare correctly as text.
- L20Only invoice 1 is overdue and unpaid.
Common mistakes
Calling real services in tests
Tests that send real emails or hit a real API fail when the network is slow and can cost money. Inject the service and pass a fake.
Mocking everything
If a test mocks every function, it only checks that the code calls what it calls today. Fake the outside world (time, network, payments), not your own logic.
Tests that depend on today
A test using the real date can pass today and fail next month. Pass a fixed date.
Quick quiz
1. Why pass `now` into a function instead of calling `datetime.now()` inside it?
Show the answer
Answer: A
Injecting the time makes the function deterministic: same input, same output.
2. A test needs to check that a welcome email was sent, without sending one. What do you pass in?
Show the answer
Answer: A
A recording fake (a spy) lets the test inspect what would have been sent.
Practice
3 exercises in three steps: predict what a program prints, change a program to meet a new requirement, then write one from scratch. Your code runs in the browser and is checked the moment you submit.
- PredictA stub bank
Type exactly what this prints.
- ModifyMake it testable
shop_status()reads the real clock, so it can't be tested reliably. Change it toshop_status(now), wherenowis adatetime, and add one rule from the owner: the shop is closed all day on Sundays (now.weekday() == 6). Don't calldatetime.now()any more. - WriteWrite a fake bank
Write a class
FakeBankto testcheckoutwith:FakeBank()approves every card;FakeBank(declined=["4000"])declines the cards listedcharge(card, amount)returnsTrueorFalse, and records every attempt inself.chargesas a tuple(card, amount)
Practise this lesson
Open the interactive lesson to edit and run these examples and solve the exercises with instant feedback. Free to start, no sign-up.
Interactive lessons open in order. If you have not done the earlier ones yet, you start at the course map.
Terms in this lesson
| Term | Chinese | Definition |
|---|---|---|
| Dependency injection | 依赖注入 | Passing in what code depends on, so tests can swap it. |
| Test double | 测试替身 | A stub, fake or mock used in place of a real dependency. |
| Mock | 模拟对象 | A double that records how it was called. |