人人都能学编程
English 开始学习
专项 · 软件测试第 3 课 / 共 5 课免费阅读

测试替身与依赖注入 · 软件测试

发邮件、扣款、依赖当前时间的代码,怎么测试?

课程目录

为什么需要它

真的邮件、真的付款、真的时钟,会让测试变慢、时好时坏,有时还要花钱:你肯定不希望每跑一次测试就扣一次卡。解决办法是一个设计习惯:让代码接收这些东西,而不是自己去拿。这样测试就可以塞进一个无害的替身,代码本身也更容易修改。

直觉理解

难测的写法:

def shop_status():
    hour = datetime.datetime.now().hour   # 自己去读真实的时钟

好测的写法,因为时间是传进来的:

def shop_status(now):
    hour = now.hour

这就是依赖注入:把函数依赖的东西(时间、邮件发送器、数据库、支付客户端)作为参数交给它。

测试时,你传入一个测试替身,而不是真家伙:

替身做什么
stub 桩返回固定的答案(「现在是 09:00」)
fake 伪实现一个能用的小版本(内存数据库)
mock / spy 模拟 / 间谍记录自己是怎么被调用的,好让测试检查

Python 的 unittest.mock 可以帮你造这些,但一个十行的小类往往更清楚。

定义

依赖注入

Passing a function or object the things it depends on, instead of letting it create or fetch them itself.
把函数或对象依赖的东西传给它,而不是让它自己去创建或获取。

测试替身

A stand-in for a real dependency in tests: a stub, fake or mock.
测试中代替真实依赖的东西:桩、伪实现或模拟对象。

示例

把时间传进来

动手试试

now 变成参数之后,任何时刻都能测试。

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
逐步执行
  1. L3这个函数不再去看真实的时钟。
  2. L8测试可以精确地挑出边界上的时刻:08:59、09:00、18:00。

伪造的邮件发送器

动手试试

伪实现把本该发出的邮件记下来,好让测试检查。

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')]
逐步执行
  1. L1和真正的发送器一样有 send(),但它只是记下来。
  2. L8代码通过参数拿到发送器和今天的日期。
  3. L10ISO 格式的日期(YYYY-MM-DD)按文字比较也是对的。
  4. L20只有 1 号发票逾期且未付。

常见错误

测试里调用真实服务

发真实邮件或调用真实 API 的测试,网络一慢就失败,还可能花钱。把服务注入进来,测试时传一个伪实现。

什么都模拟

如果一个测试把每个函数都模拟掉,它只能检查代码今天调用了什么。只伪造外部世界(时间、网络、支付),不要伪造你自己的逻辑。

依赖今天日期的测试

用真实日期的测试,今天能通过,下个月可能就失败了。传入一个固定的日期。

小测验

1. 为什么要把 `now` 传进函数,而不是在函数里调用 `datetime.now()`?

  1. A测试可以选择任意时刻,结果不会每天都变
  2. B运行得更快
  3. Cdatetime.now() 在夜里不准
查看答案

答案:A

注入时间让函数变得确定:同样的输入,同样的输出。

2. 一个测试要检查欢迎邮件发出去了,但又不能真发。你传入什么?

  1. A一个会记录消息的伪发送器
  2. B真正的发送器,加一个测试地址
  3. C什么都不传;跳过检查
查看答案

答案:A

会记录的伪实现(间谍)让测试能检查本该发出的内容。

动手练习

3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。

  1. 预测输出一个桩银行

    准确写出它打印的内容。

  2. 改需求让它可测试

    shop_status() 读的是真实时钟,没法可靠地测试。把它改成 shop_status(now),now 是一个 datetime,再加上店主的一条规则:周日全天关门(now.weekday() == 6)。不要再调用 datetime.now()。

  3. 从零写写一个伪银行

    写一个 FakeBank 类,用来测试 checkout:

    • FakeBank() 批准所有卡;FakeBank(declined=["4000"]) 拒绝列出的卡
    • charge(card, amount) 返回 True 或 False,并把每次尝试以元组 (card, amount) 记进 self.charges

在互动课里练这一课

打开互动课,这些例子都能直接改、直接运行,练习题提交马上判题。免费开始,不用注册。

互动课按顺序解锁:前面的课还没学,会先带你到课程地图。

本课术语

术语中文英文定义
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.