Codex 测试建议太泛:6 个空话来源 + 锚到签名 + 类型 + bug 历史

「测一下 happy path 和 error path」——2026 年没用——把测试绑到函数的真实签名、真实类型、过去的真 bug 上。

你让 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 #排查 #排查 #测试太泛