测试策略与测试替身
把单元、集成和端到端测试分层,并学会替换时间、网络和文件依赖。
测试策略与测试替身
Vitest 或 Jest 的 mock 思维可以迁移,但 Python 和 C++ 的依赖替换方式不同。更稳的设计是先用接口隔离外部世界,再选择 mock、fake 或真实集成测试。测试的目的不是把实现每一行重演,而是控制时间、网络、文件、随机数和设备这些不确定性,让失败可重复。跨语言迁移还要避免只测试“类型能编译”:协议和边界需要运行时样例。
学习目标
- 能识别系统时钟、随机数、网络、文件和设备等不确定依赖,并设计替身。
- 能用单元、fake 集成与少量真实边界测试覆盖不同层次的风险。
- 能构造修复前失败、修复后通过的回归用例,并避免只断言 mock 被调用。
测试不是重复点击,而是控制不确定性
下面这段代码只保留同一个意图,重点观察输入边界、数据流和失败语义,而不是逐字符翻译。
const clock = { now: () => Date.now() };
const expiresAt = clock.now() + ttl;
expect(isFresh(expiresAt, clock)).toBe(true); # Python
def is_fresh(expires_at, now):
return expires_at > now
# test
assert is_fresh(110, 100) is True
// C++
bool is_fresh(Time expires_at, Time now); 从单元测试到集成测试
纯函数优先测试,外部依赖留在少数适配器。fake 适合模拟完整协议,例如一个内存文件仓库或固定响应的模型客户端;mock 适合验证调用边界,但不应让测试绑定太多内部调用次数。集成测试连接真实序列化、文件或 HTTP 组件,少量端到端测试验证入口组合。每一层都要覆盖失败、超时、空输入、重复调用和取消。
class FixedClock:
def __init__(self, now: float):
self.now = now
def is_fresh(expires_at: float, clock: FixedClock) -> bool:
return expires_at > clock.now
assert is_fresh(110, FixedClock(100))
assert not is_fresh(100, FixedClock(100))
替换时间、网络和文件
不要在核心函数内部直接调用系统时钟、读取全局环境或创建 HTTP 客户端。把 Clock、FileStore、ModelClient 作为接口/参数注入,生产环境传真实实现,测试传固定 fake。Python 可以用 fixture 和 monkeypatch,C++ 可以用抽象类或模板注入,JS/TS 可以传函数对象;三者都应确保替身行为符合真实协议,而不是返回过于理想化的数据。
常见错误与排错思路
常见错误是只断言 mock 被调用,却没有验证输出契约;另一个是测试使用当前时间和真实网络,偶发失败后重跑就通过。排错时记录测试输入、随机种子、fake 响应和耗时,先把外部依赖替换成确定值,再逐步恢复集成层。C++ 若测试难以链接,检查接口是否包含实现细节;Python 若 patch 不生效,确认被替换的是“使用它的模块”而不是原始定义模块。
注入时间让边界测试可重复
不要让核心规则直接读取墙上时钟,否则测试结果会随运行日期变化。把当前时间作为输入或 Clock 依赖,生产环境传真实时钟,测试传固定时间;同一输入就能稳定产生同一结果。
function isExpired(expiresAtMs, nowMs) {
return expiresAtMs <= nowMs;
}
const now = 1000;
console.log(isExpired(1000, now), isExpired(1001, now)); // true false
边界含义需要先约定:这段代码把“到期时刻等于现在”定义为已过期。若业务希望包含当前时刻,应改规则并调整测试,而不是留给不同语言各自猜测。网络依赖也可注入 fake,让它立刻返回超时或固定响应,而不等待真实服务。
运行验证:测试分层与失败诊断
纯函数单测覆盖边界组合;fake 集成测试覆盖组件交互、错误映射和重试上限;真实集成测试只验证少量协议与部署假设。若测试失败,先确认 fixture、固定时钟、随机种子和 fake 响应,再判定实现回归。断言行为结果和错误类别,不把私有函数调用次数当作公开契约。
一次可靠的回归测试应先用旧实现运行并确认失败,再应用修复并确认通过;如果它在旧实现上也通过,就没有复现原问题。测试名称应描述行为和边界,例如“到期时间等于当前时间时标记过期”,而不是模糊地叫 test_case_3。
测试通过也不等于设备、生产网络或真实模型已经验收:替身只能证明替身路径。对硬件和外部服务要单独记录真实环境验收条件,避免把模拟结果误报为现场验证。
每次验收报告都应把自动化单测、构建/静态检查和真实环境试验分别列出。尤其是 C++ 驱动、生产网络与 AI 模型,fake 测试不能证明硬件时序、现场噪声或远端服务可用。
测试替身的边界与风险
Fake 应模拟调用方依赖的契约,而不是复制真实实现的全部细节。若 fake 永远返回成功,集成测试就无法证明超时、断连或格式错误时调用者如何反应;若 mock 精确断言内部方法顺序,又可能让无害重构导致大量测试失效。选替身前先写清楚:调用方观察到什么输入、结果和错误。
运行验证:从失败复现到回归保护
为一次曾经出现的故障保存最小脱敏 fixture,先运行旧逻辑确认断言失败,再运行修复后的逻辑确认通过。随后改变一个无关实现细节,确保测试仍通过;这能检查测试是否锁定公开行为而非偶然实现。对于集成测试,记录启动依赖、清理资源和超时上限,避免测试彼此污染。
契约样例与跨语言一致性
为跨进程数据维护小型 golden fixtures,每条样例注明 schema 版本、输入、预期规范值或错误类别。让 JavaScript、Python 和 C++ 的解析器共同读取这些样例,比较标准化后的结果,不比较语言各自的对象打印格式。对时间、浮点数、Unicode 和缺失字段尤其要固定解释方式。
运行验证:让测试失效时能定位
单测失败输出 fixture 名、字段路径和预期/实际差异;集成测试记录启动步骤与依赖版本。对随机化测试固定 seed 并在失败时保留 seed;对性能测试与功能测试分开报告。测试稳定通过只证明已覆盖的行为,未覆盖的设备、生产网络或真实模型仍需独立验收,不能由 mock 结果代替。
迁移练习
请完成:把依赖当前时间的过期判断改造成三个语言都容易测试的接口,并各写一个“刚好过期”和“仍然有效”的断言;再为网络超时设计 fake。
测试策略与测试替身练习
把依赖当前时间的过期判断改造成三个语言都容易测试的接口,并各写一个“刚好过期”和“仍然有效”的断言;再为网络超时设计 fake。
给我一点提示
不要在核心函数内部直接调用系统时钟;fake client 要能稳定地产生 timeout,而不是等待真实网络。
查看参考答案
将 now 作为参数或 Clock 接口注入;生产环境传真实时钟,测试传固定时间并断言 expires_at == now 为 false。网络用 FakeClient 直接抛出 TimeoutError/返回 timeout 状态,测试调用方只验证重试上限和最终错误。 本节结论
可测试性不是测试框架提供的,而是模块边界设计出来的。完成后,请分别运行纯函数测试、fake 集成测试和一条真实边界测试,确认每层职责没有混在一起。
小结
测试质量来自可控边界和明确行为,而不是 mock 数量。将时间与外部依赖注入后,用单元、fake 集成和真实边界测试逐层验证,失败就更容易重现和定位。
阶段共 8 节课,按顺序完成更容易建立完整的迁移模型。