17370845950

Python 如何写出可测试的函数
可测试函数需明确输入输出、无隐式依赖、无副作用、返回具体值、避免修改可变输入,优先纯逻辑测试而非过度mock。

函数必须有明确的输入和输出

可测试的函数不能依赖全局状态或隐式环境,比如直接读配置文件、调用 time.time()、修改模块级变量。测试时你得能“喂”进去确定的输入,拿到确定的输出。

常见错误现象:test_get_user() 有时通过有时失败,因为函数内部调用了 datetime.now() 或读了本地 config.yaml;或者函数里写了 print()logging.info(),导致断言逻辑被干扰。

  • 把所有外部依赖抽成参数:时间用 now=None,配置用 config_dict=None
  • 避免在函数体内做 I/O(如 open()requests.get()),改用传入已准备好的数据或 mock 对象
  • 返回值要具体:别返回 None 表示成功,而用布尔值或枚举;别靠打印日志判断逻辑分支

避免副作用,尤其是修改传入的可变对象

如果函数接收一个 listdict 并直接修改它,测试时容易污染其他用例,也违背“输入确定 → 输出确定”的契约。

使用场景:比如写一个 add_tag(items, tag),本意是给每个 item 加个字段,但如果它直接 item["tag"] = tag,那测试完原数据就变了。

  • 默认对可变参数做浅拷贝:items = items.copy()items = [dict(i) for i in items]
  • 更稳妥的做法是不修改输入,而是返回新结构:return [{"tag": tag, **item} for item in items]
  • 如果真需要原地修改,函数名必须明确体现副作用,比如叫 inplace_add_tag(),且文档注明“修改输入列表”

用类型提示 + 默认参数暴露行为边界

类型提示不是装饰,它是测试友好性

的基础设施。IDE 和 mypy 能帮你提前发现传错类型的问题,更重要的是——测试用例写起来更有依据。

参数差异直接影响测试覆盖粒度。比如一个函数声明为 def parse_date(s: str, default: Optional[datetime] = None),你就知道至少要测:s 是空字符串、非法格式、合法 ISO 字符串;defaultNone 和非 None 两种路径。

  • 必填参数不设默认值(除非语义上确实可选),强迫调用方思考输入完整性
  • UnionOptional 显式表达可能的类型分支,避免运行时才抛 AttributeError
  • 复杂结构优先用 TypedDictdataclass 封装,比裸 dict 更易构造测试输入

测试时别绕过主逻辑去 mock 太深

有人为了“让测试快”,把函数里所有依赖都 mock 掉,结果实际函数体一行没跑,只验证了“是否调用了 mock”。这不是测试函数,是在测试调用顺序。

性能影响:过度 mock 可能让测试失去真实意义;兼容性影响:当底层接口变更(比如第三方库升级),mock 行为和真实行为脱节,测试照过但线上炸了。

  • 优先测试函数本身:给定输入,检查返回值是否符合预期
  • 只 mock 真正难控的部分(如网络、时间、随机数),且尽量用轻量方式,比如传 now=datetime(2025,1,1) 比 mock datetime 模块更安全
  • 如果函数必须调外部服务,把它拆成两层:纯逻辑函数 + 一个薄的胶水函数负责调 requests.post(),前者专注单元测试,后者走集成测试
测试真正难的不是写 assert,而是让函数足够“干净”到你能放心地断言。越早把副作用、隐式依赖、模糊边界从函数里剔出去,后面补测试的成本就越低。