Everyone Can Learn Program
中文 Start learning
Specialty · TestingLesson 3 of 5Free to read

Fakes and dependency injection · Software Testing

How do you test code that sends emails, charges cards or depends on the time?

Course contents

Why 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 clock

Easy to test, because the time is passed in:

def shop_status(now):
    hour = now.hour

That'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:

DoubleWhat it does
stubreturns fixed answers ("the time is 09:00")
fakea small working version (an in-memory database)
mock / spyrecords 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 yourself

With 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)))
Output
closed
open
closed
Step by step
  1. L3The function no longer looks at the real clock.
  2. L8A test can pick exactly the boundary minutes: 08:59, 09:00, 18:00.

A fake mailer

Try it yourself

The 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)
Output
[('a@example.com', 'Invoice 1 is overdue')]
Step by step
  1. L1Same send() as the real mailer, but it only remembers.
  2. L8The code gets the mailer and today's date as arguments.
  3. L10ISO dates (YYYY-MM-DD) compare correctly as text.
  4. 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?

  1. ATests can choose any moment, so results don't change from day to day
  2. BIt runs faster
  3. Cdatetime.now() is wrong at night
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?

  1. AA fake mailer that records messages
  2. BThe real mailer with a test address
  3. CNothing; skip the check
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.

  1. PredictA stub bank

    Type exactly what this prints.

  2. ModifyMake it testable

    shop_status() reads the real clock, so it can't be tested reliably. Change it to shop_status(now), where now is a datetime, and add one rule from the owner: the shop is closed all day on Sundays (now.weekday() == 6). Don't call datetime.now() any more.

  3. WriteWrite a fake bank

    Write a class FakeBank to test checkout with:

    • FakeBank() approves every card; FakeBank(declined=["4000"]) declines the cards listed
    • charge(card, amount) returns True or False, and records every attempt in self.charges as 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

TermChineseDefinition
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.