Skip to main content

用途

重构是在外部行为不变的前提下改善内部结构。补测试不是追求覆盖率数字,而是把调用方依赖的真实行为写成可重复的证据。本页适合拆分职责、替换依赖、消除重复、改善查询性能,以及让 Codex 协助修改但不把判断标准交给它猜。 主线始终是:
完成的标准是:关键行为有证据,新增测试覆盖正常和边界路径,每个步骤可独立验证或回退,公开接口没有无意变化,性能没有超预算,失败时知道如何恢复。
示例使用 Python 和 pytest;其他语言沿用同样的判断顺序。命令、审批模式和 Codex 界面以本机 codex --help、项目脚本和官方文档为准。

一、先判断任务边界

如果需求同时说“整理代码”和“改变返回结果”,先拆成两个任务。行为变化必须有新契约,不能藏在重构名义下。

开始前检查

Windows 可用 PowerShell 的 Get-ChildItem -Recurse -Depth 2 -File -Include AGENTS.md,pyproject.toml,Makefile。先阅读适用的 AGENTS.md,确认测试、lint、类型检查、构建命令和隔离要求。长期约定写进 AGENTS.md,临时要求留在任务提示中。 给 Codex 的只读探索提示:

二、行为基线

什么必须记录

行为基线回答“当前代码实际上做了什么”,不是回答“理想设计应该是什么”。至少记录:
  • 函数签名、默认值、返回类型、字段名称和排序。
  • None、空集合、重复值、非法值和极端输入的处理。
  • 异常类型、消息中稳定的部分和抛出时机。
  • 查询次数、写入顺序、事务、日志、指标和外部请求。
  • 缓存命中、失效、全局状态和多次调用的差异。
  • 超时、重试、取消、并发下的表现。
  • 典型输入下的耗时、内存和网络请求数。
“并不理想但已被依赖”的行为也要记录。例如旧接口对 None 返回 None,不能因为更喜欢抛异常就直接改变它。

从外到内调查

  1. 搜索公共入口、导入路径、路由和所有调用方。
  2. 阅读现有测试、夹具、模拟对象和测试数据。
  3. 用代表性输入运行入口,保存结果和异常。
  4. 观察数据库、HTTP、日志、事件等副作用。
  5. 找出没有测试但被调用方依赖的分支。
  6. 将已确认事实、假设和待用户决定的行为分开。

基线表

快照只能捕捉整体输出,不能代替异常和副作用断言。推荐使用“小快照 + 关键字段精确断言 + 查询或事件断言”。

三、先补测试

测试优先级

先覆盖公共入口,再决定是否测试内部函数:
  • 正常路径、空输入、单项输入和最大合理输入。
  • 缺字段、额外字段、错误类型、非法值。
  • 重复、乱序、负数、零值、极端字符串。
  • 依赖返回空结果、抛异常、超时和部分失败。
  • 多次调用、重试、取消、缓存和状态残留。
  • 输出顺序、字段类型和可变性等调用方可能依赖的细节。
不要为了提高覆盖率大量锁死私有实现。测试替身也要诚实:内存仓库不能证明真实数据库的事务和排序,简单 Mock 不能证明真实 HTTP 契约。

先让回归测试失败

修已知 Bug 时,先让测试在旧代码上失败,再修实现。测试一开始就通过,常见原因是没有走到缺陷分支、断言太弱、替身绕开了错误条件,或运行错了文件。
-x 用于快速暴露首个失败。确认失败确实对应目标问题后再动实现。 参数化适合表达清楚的边界:
测试全局缓存或单例时必须清理:
如果测试依赖执行顺序,先修复状态隔离,不要调整顺序掩盖问题。

四、完整演练:重构坏味道订单模块

这个例子同时展示长函数、全局缓存、异常吞掉、N+1 查询、字段语义不一致和隐式默认值。目标是小步改善,不是一次重写。

1. 旧模块

先列出坏味道:
  1. 全局缓存让数据过期、测试污染和不同仓库实例互相影响。
  2. 一个函数处理入口兼容、缓存、异常、过滤、查询、金额和格式化。
  3. except Exception 把超时、连接失败和程序错误都伪装成空列表。
  4. 每个订单查一次明细,输入变大时产生 N+1 查询。
  5. 已删除商品不计金额,却计入 item_count,形成旧接口语义。
  6. currency 只有 CNY 分支,其他值静默返回原始金额。
  7. ID 排序可能已被调用方依赖,但代码没有说明。

2. 基线测试

先准备可观察调用次数的仓库替身:
基线至少包含:
还应记录缺价格按 0、缺数量按 1、非 CNY 不四舍五入、重复 ID 保留等现状。运行:
此时全绿只代表测试描述了旧实现,并不代表设计良好。可以保存基线提交或补丁:
不允许中途提交时可用 git diff > baseline.patch 保存证据。

3. 第一步:抽出纯逻辑

只抽出金额计算,保留默认值、异常、缓存、排序和字段语义:
将旧循环替换为 _calculate_total(items, currency),不要同时做接口和性能变化。

4. 第二步:拆分读取和组装

公共入口暂时只负责兼容入口和缓存:
运行全套测试,并额外检查查询次数。结果一样不代表副作用一样。

5. 第三步:引入显式服务和兼容适配器

旧入口的签名、字段、排序和 None 语义暂时保留。新增测试证明服务实例拥有自己的依赖和缓存。之后再决定是否删除缓存;缓存删除是行为和性能变化,需说明失效策略、命中率和预算。

6. 第四步:处理边界和接口兼容

公共接口兼容至少涵盖位置参数、关键字参数、默认值、返回字段、异常类型、导入路径、HTTP 状态码和日志字段。推荐迁移顺序:
  1. 新增内部实现,不动旧入口。
  2. 旧入口调用新实现,保留签名和结果。
  3. 新旧入口共用兼容测试。
  4. 逐个迁移调用方,观察日志和指标。
  5. 标记旧入口弃用,写明期限和替代入口。
  6. 另一个变更再删除旧入口。
不要用宽泛 **kwargs 隐藏拼写错误。兼容层应明确接受的参数和未知参数的错误。

7. 第五步:替换 N+1 查询

只有仓库契约清楚后才做批量替换。先明确空 ID、重复 ID、缺失明细、顺序、部分失败和参数数量限制。批量路径可采用:
先写单条和批量结果等价测试,再观察查询数和错误率。可以用特性开关逐步启用,不能因为“批量一定更快”就跳过测量。

五、性能与回滚

性能基线

性能也是行为的一部分:关注 p50/p95、查询次数、扫描行数、内存峰值、外部请求、超时和重试。固定输入规模、环境、预热和重复次数,记录改前改后原始数据。
至少测空输入、小输入和接近线上上限的大输入:

代码、数据和发布回滚

每个提交只做一件事:
提交前查看差异;确认没有同事改动后,未提交的明确单文件才可恢复:
共享分支不要改写历史,已提交错误用 git revert <commit>,然后重新跑测试。涉及数据库、缓存或消息 schema 时,代码回滚还不够:
  • 迁移要有向前和向后脚本,并在隔离副本演练。
  • 新字段要确保旧版本可忽略或读取。
  • 消息升级使用兼容的生产者和消费者顺序。
  • 缓存键变化要有旧键读取窗口或清理策略。
  • 特性开关默认关闭,关闭后回到已验证旧路径。
发布前确认旧版本能启动并读取当前数据,指标和错误日志有基线,回滚命令已经演练,备份时间点和恢复责任人明确,并能从原用户路径验证恢复。

六、失败分类

不要让 Codex 连续修改直到“变绿”。先保留命令、首个错误、提交号和影响范围,再分类: 失败报告应能被别人复跑:
分类前不要同时改实现、测试和配置,否则根因无法定位。

七、给 Codex 的分步提示

规划阶段:
补测试阶段:
纯重构阶段:
兼容迁移阶段:
性能阶段:

八、审查与验收

行为和兼容性

  • 公共签名、导入路径和默认值是否保留?
  • None、空集合、重复项、排序和字段类型是否改变?
  • 异常是否被吞掉、换类型或延迟抛出?
  • 日志、事件、事务和外部请求顺序是否变化?
  • 兼容适配器是否有迁移范围、负责人和退出条件?

测试

  • 测试是否在旧实现上验证过基线?
  • 是否覆盖正常、空值、重复、非法、超时和部分失败?
  • 断言是否具体但没有锁死不稳定时间戳和路径?
  • 测试是否独立运行并清理缓存、临时数据?
  • 是否错误地只测试私有实现来追求覆盖率?

重构与性能

  • 每个提交是否单一目的,是否混入无关格式化和依赖升级?
  • 新抽象是否真的减少复杂度?
  • 是否有改前改后的耗时、查询数和内存数据?
  • 批量查询是否处理空输入、重复 ID、顺序和参数上限?
  • 缓存是否有生命周期、失效和容量策略?

安全和交付

  • 测试数据和日志是否脱敏,没有密钥和真实用户数据?
  • 数据迁移、写操作和外部请求是否在隔离环境演练?
  • 特性开关、回滚脚本和恢复验证是否可执行?
  • 是否只修改任务允许的文件?
典型 Python 项目验收命令:
优先使用项目已有的 make test、make lint 和 make typecheck。记录每条命令和退出码,不要只写“测试通过”。

九、工作清单与小结

重构的安全感来自证据链:行为基线告诉你改前真实发生什么;测试锁定调用方依赖;小步变更让审查和回退变便宜;兼容设计保护下游;边界和性能测试捕捉正常路径之外的风险;失败分类避免在错误方向上连续修改;回滚准备则覆盖代码、数据、配置和发布路径。 提交前问自己:我能否用测试证明改前改后的关键行为?能否解释每处 diff 为什么存在以及如何回退?如果明天出现兼容或性能回归,是否知道先关闭哪个开关、回哪个提交、用什么命令验证恢复? 参考资料:参考/codex/14-workflows.md、参考/codex/11-agents-md.md、参考/codex/36-best-practices.md。