用途
重构是在外部行为不变的前提下改善内部结构。补测试不是追求覆盖率数字,而是把调用方依赖的真实行为写成可重复的证据。本页适合拆分职责、替换依赖、消除重复、改善查询性能,以及让 Codex 协助修改但不把判断标准交给它猜。 主线始终是:
示例使用 Python 和 pytest;其他语言沿用同样的判断顺序。命令、审批模式和 Codex 界面以本机 codex --help、项目脚本和官方文档为准。
一、先判断任务边界
如果需求同时说“整理代码”和“改变返回结果”,先拆成两个任务。行为变化必须有新契约,不能藏在重构名义下。
开始前检查
Get-ChildItem -Recurse -Depth 2 -File -Include AGENTS.md,pyproject.toml,Makefile。先阅读适用的 AGENTS.md,确认测试、lint、类型检查、构建命令和隔离要求。长期约定写进 AGENTS.md,临时要求留在任务提示中。
给 Codex 的只读探索提示:
二、行为基线
什么必须记录
行为基线回答“当前代码实际上做了什么”,不是回答“理想设计应该是什么”。至少记录:- 函数签名、默认值、返回类型、字段名称和排序。
None、空集合、重复值、非法值和极端输入的处理。- 异常类型、消息中稳定的部分和抛出时机。
- 查询次数、写入顺序、事务、日志、指标和外部请求。
- 缓存命中、失效、全局状态和多次调用的差异。
- 超时、重试、取消、并发下的表现。
- 典型输入下的耗时、内存和网络请求数。
None 返回 None,不能因为更喜欢抛异常就直接改变它。
从外到内调查
- 搜索公共入口、导入路径、路由和所有调用方。
- 阅读现有测试、夹具、模拟对象和测试数据。
- 用代表性输入运行入口,保存结果和异常。
- 观察数据库、HTTP、日志、事件等副作用。
- 找出没有测试但被调用方依赖的分支。
- 将已确认事实、假设和待用户决定的行为分开。
基线表
快照只能捕捉整体输出,不能代替异常和副作用断言。推荐使用“小快照 + 关键字段精确断言 + 查询或事件断言”。
三、先补测试
测试优先级
先覆盖公共入口,再决定是否测试内部函数:- 正常路径、空输入、单项输入和最大合理输入。
- 缺字段、额外字段、错误类型、非法值。
- 重复、乱序、负数、零值、极端字符串。
- 依赖返回空结果、抛异常、超时和部分失败。
- 多次调用、重试、取消、缓存和状态残留。
- 输出顺序、字段类型和可变性等调用方可能依赖的细节。
先让回归测试失败
修已知 Bug 时,先让测试在旧代码上失败,再修实现。测试一开始就通过,常见原因是没有走到缺陷分支、断言太弱、替身绕开了错误条件,或运行错了文件。-x 用于快速暴露首个失败。确认失败确实对应目标问题后再动实现。
参数化适合表达清楚的边界:
四、完整演练:重构坏味道订单模块
这个例子同时展示长函数、全局缓存、异常吞掉、N+1 查询、字段语义不一致和隐式默认值。目标是小步改善,不是一次重写。1. 旧模块
- 全局缓存让数据过期、测试污染和不同仓库实例互相影响。
- 一个函数处理入口兼容、缓存、异常、过滤、查询、金额和格式化。
except Exception把超时、连接失败和程序错误都伪装成空列表。- 每个订单查一次明细,输入变大时产生 N+1 查询。
- 已删除商品不计金额,却计入
item_count,形成旧接口语义。 currency只有 CNY 分支,其他值静默返回原始金额。- ID 排序可能已被调用方依赖,但代码没有说明。
2. 基线测试
先准备可观察调用次数的仓库替身:git diff > baseline.patch 保存证据。
3. 第一步:抽出纯逻辑
只抽出金额计算,保留默认值、异常、缓存、排序和字段语义:_calculate_total(items, currency),不要同时做接口和性能变化。
4. 第二步:拆分读取和组装
5. 第三步:引入显式服务和兼容适配器
None 语义暂时保留。新增测试证明服务实例拥有自己的依赖和缓存。之后再决定是否删除缓存;缓存删除是行为和性能变化,需说明失效策略、命中率和预算。
6. 第四步:处理边界和接口兼容
公共接口兼容至少涵盖位置参数、关键字参数、默认值、返回字段、异常类型、导入路径、HTTP 状态码和日志字段。推荐迁移顺序:
- 新增内部实现,不动旧入口。
- 旧入口调用新实现,保留签名和结果。
- 新旧入口共用兼容测试。
- 逐个迁移调用方,观察日志和指标。
- 标记旧入口弃用,写明期限和替代入口。
- 另一个变更再删除旧入口。
**kwargs 隐藏拼写错误。兼容层应明确接受的参数和未知参数的错误。
7. 第五步:替换 N+1 查询
只有仓库契约清楚后才做批量替换。先明确空 ID、重复 ID、缺失明细、顺序、部分失败和参数数量限制。批量路径可采用:五、性能与回滚
性能基线
性能也是行为的一部分:关注 p50/p95、查询次数、扫描行数、内存峰值、外部请求、超时和重试。固定输入规模、环境、预热和重复次数,记录改前改后原始数据。代码、数据和发布回滚
每个提交只做一件事:git revert <commit>,然后重新跑测试。涉及数据库、缓存或消息 schema 时,代码回滚还不够:
- 迁移要有向前和向后脚本,并在隔离副本演练。
- 新字段要确保旧版本可忽略或读取。
- 消息升级使用兼容的生产者和消费者顺序。
- 缓存键变化要有旧键读取窗口或清理策略。
- 特性开关默认关闭,关闭后回到已验证旧路径。
六、失败分类
不要让 Codex 连续修改直到“变绿”。先保留命令、首个错误、提交号和影响范围,再分类:
失败报告应能被别人复跑:
七、给 Codex 的分步提示
规划阶段:八、审查与验收
行为和兼容性
- 公共签名、导入路径和默认值是否保留?
None、空集合、重复项、排序和字段类型是否改变?- 异常是否被吞掉、换类型或延迟抛出?
- 日志、事件、事务和外部请求顺序是否变化?
- 兼容适配器是否有迁移范围、负责人和退出条件?
测试
- 测试是否在旧实现上验证过基线?
- 是否覆盖正常、空值、重复、非法、超时和部分失败?
- 断言是否具体但没有锁死不稳定时间戳和路径?
- 测试是否独立运行并清理缓存、临时数据?
- 是否错误地只测试私有实现来追求覆盖率?
重构与性能
- 每个提交是否单一目的,是否混入无关格式化和依赖升级?
- 新抽象是否真的减少复杂度?
- 是否有改前改后的耗时、查询数和内存数据?
- 批量查询是否处理空输入、重复 ID、顺序和参数上限?
- 缓存是否有生命周期、失效和容量策略?
安全和交付
- 测试数据和日志是否脱敏,没有密钥和真实用户数据?
- 数据迁移、写操作和外部请求是否在隔离环境演练?
- 特性开关、回滚脚本和恢复验证是否可执行?
- 是否只修改任务允许的文件?
make test、make lint 和 make typecheck。记录每条命令和退出码,不要只写“测试通过”。
九、工作清单与小结
参考/codex/14-workflows.md、参考/codex/11-agents-md.md、参考/codex/36-best-practices.md。