Python 可维护测试代码的编写原则

2次阅读

测试函数名须见名知意并强绑定被测逻辑,推荐test_被测函数名_when_条件_then_预期结果模式;禁用全局mock;断言需具体到字段;测试数据优先用字面量构造。

Python 可维护测试代码的编写原则

测试函数名必须见名知意,且和被测逻辑强绑定

很多人写 test_something()test_func(),跑通就完事。结果半年后自己都看不懂这个 test 在验什么,更别说别人。函数名不是占位符,是契约——它得说清“在什么条件下,输入什么,预期输出/行为是什么”。

实操建议:

立即学习Python免费学习笔记(深入)”;

  • test_[被测函数名]_when_[条件]_then_[预期结果] 模式,比如 test_calculate_discount_when_user_is_vip_then_returns_20_percent
  • 避免泛化词:不用 test_valid_input,改用 test_parse_date_when_input_is_iso_format_then_returns_datetime_object
  • 如果条件分支多,宁可拆成多个函数,也不要在一个 test 里塞 if 判断

别在测试里 mock 全局状态或时间,优先用参数注入

看到 patch('datetime.datetime')mock.patch.dict('os.environ') 就该警觉——这类 mock 很容易让测试变成“只在当前环境能过”的脆弱快照。一旦项目引入并发、多进程或配置热加载,测试就开始随机失败。

实操建议:

立即学习Python免费学习笔记(深入)”;

  • 把时间、配置、http 客户端等依赖,作为参数传给被测函数(哪怕默认值保持原样),测试时直接传入可控的 fake 对象
  • 例如:把 def send_notification(): ... datetime.now() ... 改成 def send_notification(clock=datetime.datetime): ... clock.now() ...
  • 全局 patch 只保留在集成测试层,单元测试中尽量让它不存在

assert 语句必须指向具体字段,禁用 assertEqual(dict1, dict2)

self.assertEqual(resp, expected_resp) 看似省事,但一旦失败,你只能看到两坨超长 diff,根本看不出到底是 status_code 错了,还是 data.user.id 多了个空格,或是 timestamp 因时区偏差差了 1 秒。维护成本直接翻倍。

实操建议:

立即学习Python免费学习笔记(深入)”;

  • 拆开断言:先 self.assertEqual(resp.status_code, 200),再 self.assertEqual(resp.json()['user']['name'], 'alice')
  • 对嵌套结构,用 assertDictEqual 前先做浅层校验,或用 pytest-asyncio 配合 deepdiff(仅限调试期,不进主测试流)
  • 对浮点数、时间戳等易漂移字段,用 assertAlmostEqual 或自定义容差比较,而不是硬比字符串

测试数据优先用字面量构造,少用工厂函数或数据库 fixture

一个 UserFactory.create() 调用背后可能触发 5 层 ORM 关联、3 个信号、2 次 redis 写入——这已经不是单元测试,是微型集成测试。启动慢、失败难定位、还容易污染其他 test 的 DB 状态。

实操建议:

立即学习Python免费学习笔记(深入)”;

  • 90% 场景下,直接写 {'id': 1, 'email': 'test@example.com', 'is_active': True} 更快、更稳、更易读
  • 只有当对象结构复杂且多处复用时,才考虑轻量 factory(如 make_user(is_staff=True)),且禁止带 side effect
  • 数据库 fixture 仅用于真正需要验证 sql 行为的测试(如迁移脚本、raw query),并确保每个 test 用独立 schema 或事务 rollback

最常被忽略的一点:测试可维护性不取决于覆盖率数字,而取决于修改一行业务逻辑后,你能多快定位到哪几个 test 需要同步更新——这靠的是命名、隔离和断言粒度,不是靠 mock 越多越“像真实环境”。

text=ZqhQzanResources