你让 Codex 给 parseInvoice(input: InvoiceRaw): Invoice 出测试建议,它返回:「测 happy path、测 error path、测 edge case、测 boundary、测非法输入」。没一条引用真实签名,没一条提 InvoiceRaw 真实形状或你 domain 里”非法”的定义——这些建议套到任何函数上都成立。
泛测试建议来自泛 prompt。修法:把请求 ground 到真签名 + 真类型 + 真 bug 历史。一条覆盖过去真出过的 bug 的回归测试,比 10 条假想 edge case 测试都有价值。
常见原因
按命中率从高到低:
1. Prompt 没指名被测函数
「给这个文件出测试建议」+ 一个 200 行的文件——Codex 对冲,列出适用于”文件里所有函数”的测试。结果就是通用模板、不是函数特定。
如何判断:回看 prompt——没用 name + path 指名被测函数,Codex 就按整文件工作。
2. Codex 看了函数但没读类型定义
函数收 InvoiceRaw。Codex 没打开 InvoiceRaw 的定义(在 types/invoice.ts)——所以它不知道哪些字段可选、有哪些 enum、合法性合同是什么。
如何判断:建议把所有输入当通用对象——没提具体字段、enum 值、约束。
3. Codex 没检查现有测试
现有测试已经盖了 happy path,Codex 还建议「加 happy path 测试」——重复。或者它用 jest.mock 但你现有测试用 vi.mock。
如何判断:建议和现有测试 diff——50% 重复 / 框架错——prompt 没让它”先读现有测试”。
4. 没喂 bug 历史
Codex 不知道你这块有过 3 个 bug:时区 bug、闰日 bug、Unicode normalization bug——建议里一个回归测试都没有。
如何判断:建议对照 git log --grep="fix" 的关闭 bug 列表——已知 bug 形状没盖到,就是你没喂进去。
5. Mock 配置错了,因为 Codex 猜了依赖
Codex 建议 mock axios——但你代码用 ky。或建议 mock 数据库——但你测试用真测试 DB。Mock 形状对、目标错。
如何判断:建议测试里的 import 和这块真测试里的 import 对不上。
6. 用 coverage 思维盖过实用思维
“为第 47 行加测试”——但第 47 行是 if (debug) console.log(...)。测试没价值,只是冲覆盖率。Codex 过度看重 coverage 指标、忽略真实风险。
如何判断:建议测试瞄准琐碎分支(log、fallback string)——是冲覆盖率不是降风险。
最短修复路径
按收益从高到低,前 2 步把泛建议变成可执行测试。
Step 1:锚到函数 + 类型 + 现有测试
模板:
为 `src/parsers/invoice.ts` 的 `parseInvoice(input: InvoiceRaw): Invoice` 出测试建议。
建议前先:
1. 读 `src/parsers/invoice.ts`,引述函数签名。
2. 读 `src/types/invoice.ts`,引述 `InvoiceRaw` 和 `Invoice` 类型。
3. 读 `src/parsers/invoice.test.ts`,总结已覆盖什么。
然后建议 5 个**新**测试(不重复现有覆盖):
- 每条指明具体输入形状 + 期望输出。
- 用真实类型——不要泛 `any` 或编字段名。
- 风格照现有文件(vitest、`assert.deepStrictEqual`)。
「先读」逼它接地。
Step 2:喂 bug 历史
这块过去的 bug(从 `git log --grep="fix.*invoice"`):
- 2026-01:闰日 "2024-02-29" 解析失败
- 2026-03:Unicode normalization(NFC vs NFD)导致字段名匹配失败
- 2026-04:空数组 `lineItems: []` 返回 NaN 总价而不是 0
为每条出一个回归测试。放在 `describe("regression", ...)` 块里。
已知 bug 的回归测试比 10 条假想 edge case 强。
Step 3:要对抗式输入
`parseInvoice` 能收到但仍算合法的最坏输入是什么?
- 最长可能字段
- 空数组 / 空字符串
- Unicode surrogate pair
- JS `Number` 边界附近(Infinity、MAX_SAFE_INTEGER)
- 错配的 currency / locale
每条写一个测试:输入 + 期望行为。
Step 4:要 property-based 显式说
用 `fast-check` 出 3 条 property-based 测试:
- Property 1:round-trip——任意合法 InvoiceRaw,parse(serialize(x)) === x。
- Property 2:sum 不变量——total === sum of lineItems.amount。
- Property 3:currency 一致——所有 line item 都和 invoice 的 currency 一致。
每条给出 `fast-check` setup + 断言。
Step 5:建议跑不通就拒收
Codex 给完建议,落到测试文件里跑:
pnpm vitest run src/parsers/invoice.test.ts --reporter=verbose
编不过(引用了不存在的字段、类型错)就拒收:
测试 `parseInvoice handles tax: undefined` 用了 `input.tax`——但类型是 `taxes: TaxLine[]`(复数)。按真类型重写。
session 里训练它 ground 到真类型。
Step 6:新函数让 Codex 先写测试
Greenfield 函数:
你要实现 `parseInvoice`。实现**之前**:
1. 读 InvoiceRaw 和 Invoice 类型定义。
2. 写 6 个测试覆盖:happy path、每个 enum 分支、boundary、unicode、闰年、error throw。
3. 测试现在应该编不过(还没实现)——这是预期。
4. 然后实现函数让所有测试 pass。
测试先行比测试后补能挤出更紧的覆盖。
预防建议
- 每个测试 prompt 锚到 函数签名 + 类型 + 现有测试文件
- 每个区域维护一份”已知 bug 形状”doc,prompt 里引用——避免回归 bug 再被忘
- 强制建议引用真实字段 / 类型——出现
any或编字段直接拒 - 建议立刻跑一遍,编不过的拒
- 新代码用 Codex 测试先行——比测试后补更紧
- 覆盖率不是目标,每条测试 catch bug 数才是——只冲覆盖的测试删掉
相关阅读
标签: #Codex #Coding Agent #排查 #排查 #测试太泛