同一条 AI 应用测试昨天通过、今天失败,原因可能只是外部模型响应变了、工具 API 超时,或测试账户额度不足。Mock 的价值不是假装外部服务永远正确,而是让本地逻辑的成功、超时和坏响应可以稳定重现。本文给出一个只依赖 Python 标准库的最小例子。
适合读者:已有 HTTP/API 调用和单元测试经验,正在为模型、工具或支付接口写测试的开发者。示例使用 Python 3 的 unittest。
Why:外部波动不该掩盖本地缺陷
如果每个单元测试都访问真实模型,失败时很难判断是业务代码、网络、配额还是模型输出变化。反过来,如果所有测试都只用 Mock,又可能漏掉真实 API 的字段、认证和限流变化。两类测试回答的问题不同:单元测试保证自己的分支逻辑,契约测试保证与外部服务的接口仍兼容。
What:在明确边界处替换依赖
业务函数 → client.complete() → 外部模型/API
│ │
└─ 单元测试注入 FakeClient:成功 / 超时 / 坏响应
└─ 独立契约测试:少量真实请求核对字段
Mock 应放在自己控制的客户端接口上,而不是把整段业务逻辑一并替换。录制回放可保存经过脱敏的响应样本,用于重现复杂结构;样本需标注 API 版本和采集条件,并定期用真实契约测试校对。不要把密钥、用户提示词或支付信息原样提交到仓库。
How:三种结果都能稳定复现
保存为 test_classifier.py,运行 python3 -m unittest -q test_classifier.py。下面的 FakeClient 不访问网络,三项测试分别覆盖正常响应、超时和无效字段。
import unittest
def classify(client, text, timeout_s):
try:
payload = client.complete(text, timeout=timeout_s)
except TimeoutError:
return "retry"
if not isinstance(payload, dict) or payload.get("label") not in {"ok", "review"}:
raise ValueError("模型响应缺少有效 label")
return payload["label"]
class FakeClient:
def __init__(self, result=None, error=None):
self.result, self.error = result, error
def complete(self, text, timeout):
if self.error:
raise self.error
return self.result
class ClassifierTest(unittest.TestCase):
def test_success(self):
self.assertEqual(classify(FakeClient({"label": "ok"}), "sample", 1), "ok")
def test_timeout(self):
self.assertEqual(classify(FakeClient(error=TimeoutError()), "sample", 1), "retry")
def test_bad_response(self):
with self.assertRaises(ValueError):
classify(FakeClient({"label": "unknown"}), "sample", 1)
if __name__ == "__main__":
unittest.main()
示例中的超时参数 1 只是测试输入,不是生产推荐值。真实系统应根据接口预算和用户体验决定超时、重试与降级策略;重试还要考虑幂等性,尤其是支付或其他写入动作。
接入 CI 时别漏掉这些坑
- 单元测试每次提交运行,固定输入与断言;不要依赖模型“可能说出某句话”。
- 对超时、限流、服务错误、字段缺失分别注入故障,核对重试或降级是否符合产品契约。
- 用独立的真实服务契约测试检查认证、请求字段和响应结构,并将凭据留在安全的 CI 配置中。
- 录制回放样本先脱敏,明确过期与更新方式;Mock 与真实服务出现差异时以真实接口为准。
结论:Mock 负责稳定地证明你的代码如何处理已知情况,真实契约测试负责发现接口漂移。延伸阅读:Python unittest.mock 文档、pytest 文档。
