测试替身与依赖注入 · 软件测试
发邮件、扣款、依赖当前时间的代码,怎么测试?
课程目录为什么需要它
真的邮件、真的付款、真的时钟,会让测试变慢、时好时坏,有时还要花钱:你肯定不希望每跑一次测试就扣一次卡。解决办法是一个设计习惯:让代码接收这些东西,而不是自己去拿。这样测试就可以塞进一个无害的替身,代码本身也更容易修改。
直觉理解
难测的写法:
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
逐步执行
- L3这个函数不再去看真实的时钟。
- 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')]
逐步执行
- L1和真正的发送器一样有 send(),但它只是记下来。
- L8代码通过参数拿到发送器和今天的日期。
- L10ISO 格式的日期(YYYY-MM-DD)按文字比较也是对的。
- L20只有 1 号发票逾期且未付。
常见错误
测试里调用真实服务
发真实邮件或调用真实 API 的测试,网络一慢就失败,还可能花钱。把服务注入进来,测试时传一个伪实现。
什么都模拟
如果一个测试把每个函数都模拟掉,它只能检查代码今天调用了什么。只伪造外部世界(时间、网络、支付),不要伪造你自己的逻辑。
依赖今天日期的测试
用真实日期的测试,今天能通过,下个月可能就失败了。传入一个固定的日期。
小测验
1. 为什么要把 `now` 传进函数,而不是在函数里调用 `datetime.now()`?
查看答案
答案:A
注入时间让函数变得确定:同样的输入,同样的输出。
2. 一个测试要检查欢迎邮件发出去了,但又不能真发。你传入什么?
查看答案
答案:A
会记录的伪实现(间谍)让测试能检查本该发出的内容。
动手练习
3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。
- 预测输出一个桩银行
准确写出它打印的内容。
- 改需求让它可测试
shop_status()读的是真实时钟,没法可靠地测试。把它改成shop_status(now),now是一个datetime,再加上店主的一条规则:周日全天关门(now.weekday() == 6)。不要再调用datetime.now()。 - 从零写写一个伪银行
写一个
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. |