
honest-tests 推出 CLI 与 agent skill,用变异测试识别 AI 编码工具生成的假测试
来源标题:Honest-tests – detect fake tests and prove them with mutation testing
honest-tests 是一个 agent skill 加 CLI 工具,用于阻止 Claude Code、Codex、Cursor 等 AI 编码工具编写假测试,并通过变异代码验证现有测试是否真的会失败。它提供 audit 命令找出无法失败的测试,以及 mutate 命令破坏代码后观察测试是否察觉。该工具以 npx @mintwork/honest-tests 方式调用,可检测自我比较、无断言、错误断言被 try/catch 吞掉、只测 mock 等假测试模式。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
honest-tests 是一个 agent skill + CLI,用来阻止 Claude Code、Codex、Cursor 等工具写出假测试——并通过变异你的代码、观察测试是否仍然保持绿色,来证明你现有测试中哪些是假的。
npx @mintwork/honest-tests audit . # 找出永远不会失败的测试
npx @mintwork/honest-tests mutate src/pricing.ts # 证明它:破坏代码,看谁会发现
问题所在
让 agent「加一些测试」,你会得到满屏的绿色对勾。凑近看看:
it('handles multiple items', () => {
const result = subtotal(items);
expect(result).toBe(result); // 把一个值和它自己比较
});
it('applies a percentage discount', () => {
applyDiscount(100, 10); // 完全没有断言
});
it('computes the discounted price', () => {
try {
expect(applyDiscount(100, 20)).toBe(70); // 错了(应该是 80)——但依然通过
} catch {}
});
it('gives members free shipping', () => {
const qualifies = vi.fn().mockReturnValue(true);
expect(qualifies(10, true)).toBe(true); // 测试的是它刚刚配置的 mock
});
每一个都能通过。覆盖率上升了。什么都没被验证。
$ npx @mintwork/honest-tests audit test/pricing.test.ts
test/pricing.test.ts
16:5 error 把 result 和它自己比较——这永远不会失败。 tautological-assertion
│ expect(result).toBe(result);
25:1 error 测试 "applies a percentage discount" 没有任何断言——它只能因为崩溃而失败。 no-assertions
│ it('applies a percentage discount', () => {
39:7 error 断言位于会吞掉错误的 try/catch 中——当它失败时,catch 吃掉了 AssertionError,测试照样通过。 swallowed-assertion
│ expect(applyDiscount(100, 20)).toBe(70); // 错了!但 catch 把它藏起来了
53:5 error 对 mock qualifies 的返回值做断言(在第 52 行配置)——这验证的是 mock,不是你的代码。 asserts-on-mock
│ expect(qualifies(10, true)).toBe(true);
... 还有 5 条
10 个测试中有 7 个不会失败(或者根本没有测试它们声称要测的东西)。
1 个文件,10 个测试:7 个错误,2 个警告。用 --verbose 查看修复建议。
然后是杀手级演示——内置的变异测试。honest-tests 会在你的源码中(临时副本上)翻转运算符、布尔值、常量和返回值,运行你的测试,并展示每一个没人注意到的改动:
$ npx @mintwork/honest-tests mutate src/pricing.ts
22 个变异体存活——你的测试实际上没有检查这些行:
src/pricing.ts:15 if (percent < 0 || percent > 100) {
✗ < → <=
✗ || → &&
✗ > → >=
✗ 100 → 101
src/pricing.ts:22 return isMember || total >= 50;
✗ >= → >
✗ 50 → 51
...
变异分数:15.4%(26 个中杀死 4 个,超时 0 个,存活 22 个;基线 0.5s)
之后:同样的代码,诚实的测试
it('takes the percentage off the amount', () => {
expect(applyDiscount(200, 25)).toBe(150);
});
it('accepts the 0% and 100% boundaries', () => {
expect(applyDiscount(200, 0)).toBe(200);
expect(applyDiscount(200, 100)).toBe(0);
});
it('rejects percentages outside 0..100', () => {
expect(() => applyDiscount(200, -1)).toThrow(RangeError);
expect(() => applyDiscount(200, 101)).toThrow(RangeError);
});
it('starts free shipping exactly at 50 for non-members', () => {
expect(qualifiesForFreeShipping(49.99, false)).toBe(false);
expect(qualifiesForFreeShipping(50, false)).toBe(true);
});
$ npx @mintwork/honest-tests audit test && npx @mintwork/honest-tests mutate src/pricing.ts
✓ 2 个文件中的 12 个测试:没有发现假测试。
变异分数:100%(26 个中杀死 26 个,超时 0 个,存活 0 个;基线 0.5s)
✓ 每一个变异都被捕获了。这些测试确实在测试一些东西。
两个项目都放在 fixtures/ 下——你可以自己运行 npm run demo:bad / npm run demo:good
(npm run demo:gif 会根据真实的 CLI 输出重新渲染上面的 GIF)。
Claude Code(plugin + skill)
/plugin marketplace add mintwork/honest-tests
/plugin install honest-tests@honest-tests
或者只把 skill 放进仓库:npx @mintwork/honest-tests init claude(写入 .claude/skills/honest-tests/)。
然后说:「为 src/cart.ts 写测试」或「审查我的测试」——或者使用斜杠命令
/honest-tests:audit [path] 和 /honest-tests:mutate <file>。
npx @mintwork/honest-tests init codex # 向 AGENTS.md 追加 honest-tests 段落(幂等)
Cursor
npx @mintwork/honest-tests init cursor # 写入 .cursor/rules/honest-tests.mdc(自动附加到测试文件)
任意 agent / CI
npx @mintwork/honest-tests audit . --json # 机器可读的发现
npx @mintwork/honest-tests mutate src/x.ts --min-score 80 # 变异分数低于 80% 时让 CI 失败
这个 skill 如何工作
该 skill(Agent Skills 格式)教给 agent 一条规则——一个测试只有在它所声称的行为被破坏时失败,才算诚实——外加两个工作流:
agent 按需加载的参考文档:测试坏味道目录、
编写诚实的测试、
审查流程、
变异测试。
规则
严重级别
捕获内容
no-assertions
error
测试体从不做断言——除非崩溃,否则通过
tautological-assertion
error
expect(x).toBe(x)、expect(f(1)).toBe(f(1))、expect(true).toBe(true)、toHaveLength(list.length)
asserts-on-mock
error
对测试自己配置的 mock 的返回值做断言(vi.fn().mockReturnValue、spyOn、vi.mocked、.mock.results)
asserts-on-own-literal
error
对测试刚刚构造的字面量做断言:const p = { service: 's3' }; expect(p.service).toBe('s3')——常见于运行时的「类型测试」
never-failing-assertion
error
expect(x).toBeTruthy(从未被调用)、裸 expect(x)、未 await 的 .resolves/.rejects、typeof x 为「defined」、length >= 0、{} 为 truthy
swallowed-assertion
error
断言位于 try 中,而 catch 没有重新抛出
catch-only-assertion
error
断言只在 catch 中——没有抛出任何东西时也通过
mocked-unit-under-test
error
在 cart.test.ts 中写 vi.mock('./cart');对被测单元自身的导出进行 spy 并替换
focused-test
error
留下了 .only、fit、fdescribe
excessive-mocking
warning
一个文件中 5 个以上的模块 mock
snapshot-only
warning
唯一的断言是 snapshot
conditional-assertion
warning
断言位于 if / && / 三元表达式之后
skipped-test
warning
.skip、xit、xdescribe
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。