开头:先给你一个 30 秒的自测
打开你的项目,跑这个命令:
npx ai-test-check ./src
如果输出里出现「测试有效性: 40%」这种数字,说明:
你改坏 10 处代码,测试只能抓到 4 处。
剩下 6 处,你的测试看不见。
这就是我说的「假绿」:测试全绿,但代码是错的。
一、先看一个具体例子(30 秒看懂问题)
源码:
function isAdult(age) {
return age >= 18;
}
AI 写的测试:
test('isAdult works', () => {
expect(isAdult(20)).toBe(true);
});
测试通过,覆盖率 100%,CI 全绿。
现在我把 >= 改成 >:
function isAdult(age) {
return age > 18;
}
测试还是绿的。
因为 20 岁在两种写法下都返回 true。而 18 岁的人,会拿到错误的答案,没有任何测试能发现。
二、为什么覆盖率发现不了这个问题
| 工具 | 它回答的问题 | 它不回答的 |
|---|---|---|
| Jest / Vitest | 测试跑过了吗 | 测试有效吗 |
| Coverage | 代码被执行了吗 | 执行了但断言对吗 |
| ESLint | 代码风格对吗 | 逻辑对吗 |
| TypeScript | 类型对吗 | 运行时行为对吗 |
覆盖率 100% 只能说明「每行代码都被执行过」,不能说明「每行代码的改动都能被发现」。
三、什么是变异测试(3 分钟看懂)
原理四步:
① 找到源码里的"可变异点"(比较运算符、逻辑运算符)
② 逐个改坏
③ 跑测试
测试变红 ⇒ "被杀" ⇒ 测试有效 ✅
测试还绿 ⇒ "存活" ⇒ 测试有洞 ❌
④ 恢复源码
最后算一个数:
测试有效性 = 被杀数 / 总变异数
这个数说明:你改坏 100 处代码,测试能抓到几处。
| 有效性 | 含义 |
|---|---|
90% | 测试是可信的 |
50% | 一半的改动测试看不见 |
20% | 测试基本是摆设 |
四、★ 手把手:5 分钟在你的项目上跑一遍
4.1 安装
npm install -g ai-test-check
# 或直接用 npx,不用安装
npx ai-test-check ./src
4.2 第一次跑
cd your-project
ai-test-check ./src --max=10
参数说明:
./src 要检测的源码目录
--max=10 最多变异 10 处(数字越大越准,但越慢)
4.3 预期输出
═══ AI 测试有效性检测 ═══
目录: /your/project
测试命令: npm test
最多变异: 10 处
源文件: 12 个
── 基线检查 ──
✅ 基线测试通过(可以开始变异)
可变异点: 86 个,本次抽 10 个
── 逐个变异 ──
[1/10] src/calc.js >= → > ❌ 存活(测试没抓到)
[2/10] src/calc.js === → !== ✅ 被杀(测试抓到了)
...
═══ 结果 ═══
变异总数: 10
被杀: 4(测试抓到了)
存活: 6(测试没抓到)
★ 测试有效性: 40%
存活的变异(这些地方测试有洞):
src/calc.js:4 >= → >
src/calc.js:8 >= → >
src/calc.js:15 >= → >
4.4 ★ 常见报错与解决
| 报错 | 原因 | 解决 |
|---|---|---|
❌ 基线测试就是红的 | 你的测试本来就失败了 | 先让测试全绿 |
❌ 找不到测试命令 | 没有 package.json 的 scripts.test | 用 --test-cmd="<你的命令>" |
⚠ N 个文件解析失败 | 有些文件是 ES5 或 TS | 正常,跳过(会报告) |
可变异点: 0 个 | 源码里没有运算符 | 换一个有逻辑的目录 |
五、★ 怎么读结果(这一步最重要)
5.1 只看一个数不够,要看「哪些变异存活」
光看「有效性 40%」没用,要看具体是哪几处存活。
举例:
存活的变异:
src/auth.js:23 >= → > ← 边界条件
src/auth.js:45 && → || ← 逻辑组合
src/calc.js:8 === → !== ← 相等判断
每一行都在告诉你「哪类测试没写」:
| 存活的变异类型 | 说明什么 | 该补什么测试 |
|---|---|---|
>= → > | 边界条件没测 | 补「等于」的情况 |
&& → || | 逻辑组合没测 | 补「只满足一个条件」的情况 |
=== → !== | 相等判断没测 | 补「不相等」的情况 |
5.2 有效性多少才算好
| 有效性 | 判断 | 该做什么 |
|---|---|---|
| ≥80% | ✅ 可信 | 保持 |
| 50-79% | ⚠ 中等 | 补边界测试 |
| <50% | ❌ 有洞 | 系统性补测试 |
★ 但注意:0% 不代表「测试完全没用」,只代表「这 10 个改动测试都没抓到」。
六、★ 真实数据:我跑了 8 个开源项目
这部分是「证据」,不是「主角」。你不需要关心我跑了什么,你只需要知道这个现象有多普遍。
| # | 项目 | Star | 变异数 | 被杀 | 有效性 |
|---|---|---|---|---|---|
| 1 | browserify/events | 1.4k | 10 | 10 | 100% |
| 2 | sindresorhus/slugify | 2.7k | 10 | 6 | 60% |
| 3 | jonschlinkert/is-number | 286 | 10 | 2 | 20% |
| 4 | browserify/sha.js | 298 | 10 | 0 | 0% |
| 5 | crypto-browserify/createHash | 54 | 8 | 0 | 0% |
| 6 | chalk/chalk | 23.3k | 10 | 0 | 0% |
| 7 | ljharb/object.assign | 106 | 10 | 0 | 0% |
| 8 | browserify/parse-asn1 | 20 | 10 | 0 | 0% |
| 平均 | ≈23% |
★ 最扎眼的两行:
chalk(2.3 万 star)有效性 0% —— 10 个变异全部存活browserify/events(1.4k star)有效性 100% —— 10 个全部被杀
同样是小工具库,差距从 0% 到 100%。
★ 诚实边界:
- 只跑了 8 个(不是 10 个),7 个仓库因网络/基线问题跳过
- 每个仓库只随机抽 10 个变异(不是全量)
- 「0%」不代表「测试完全没用」
- 变异可能造出语法错误,被当成「被杀」(方向保守,会高估有效性)
七、★ 为什么 AI 写的测试特别容易这样
三个原因:
原因 1:AI 只写 happy path
// AI 常写
test('discount works', () => {
expect(discount(200)).toBe(180);
});
// AI 很少写
test('discount 边界:99 不打折', () => {
expect(discount(99)).toBe(99);
});
原因 2:断言太弱
// 弱断言:只测「不报错」
expect(() => fn()).not.toThrow();
// 强断言:测「返回值对不对」
expect(fn()).toBe(expected);
原因 3:★ AI 代码和 AI 测试共享同样的误解
这是最根本的原因。
AI 写代码时理解错了需求
AI 写测试时用了同样的错误理解
⇒ 测试「验证」了错误行为
⇒ 全绿,但代码是错的
★ 有个 HN 用户的原话(我研究时找到的):
「For 100% of the root cause analyses for critical production defects that I've done over the past 18 months or so, a key finding was that AI-generated tests asserted that the defective behavior was correct.」
(18 个月内我做的所有关键生产缺陷根因分析,一个关键发现都是:AI 生成的测试断言了错误行为是正确的。)
—— Hacker News 2026-09-15
注意他的措辞:不是「没测到」,是「断言了错误行为是正确的」—— 测了,但测反了。
八、★ 怎么修:三条可执行建议
建议 1:每个 > 想一下「等于」的情况
// 有这行
if (age >= 18) { ... }
// 就该有这两个测试
test('18 岁可以', () => expect(isAdult(18)).toBe(true));
test('17 岁不可以', () => expect(isAdult(17)).toBe(false));
建议 2:每个 && 想一下「只满足一个」
// 有这行
if (age >= 18 && citizen === true) { ... }
// 就该有四个测试
test('18 岁 + 公民 → 可以', ...);
test('18 岁 + 非公民 → 不可以', ...);
test('17 岁 + 公民 → 不可以', ...);
test('17 岁 + 非公民 → 不可以', ...);
建议 3:把有效性加进 CI
# .github/workflows/test.yml
- name: Check test effectiveness
run: npx ai-test-check ./src --max=20
退出码:0 = 有效率 ≥50%;1 = <50%(可用来卡 PR)
九、工具与代码
npm 包:
npx ai-test-check ./src
GitHub:github.com/moqinghua20…
v0.2.0 更新:改用 AST 解析(acorn)找变异点。旧版纯正则会误报 —— 把正则字面量里的 (?!=) 当成 != 比较。实测:同一份代码,旧版找到 14 个变异点(9 个是假的),AST 版找到 5 个(全部准确)。
十、诚实:这个工具也有局限
| 局限 | 说明 |
|---|---|
| 只支持 JS/CJS | 用 acorn 做 AST(ES2022),不做 TS/Python |
| 只变异 8 类运算符 | >= <= > < === !== && || |
| 只换运算符 | 不改字面量/返回值/条件取反 |
| 慢 | 每个变异都要跑一次测试 |
| 不测边界值 | 只改代码,不生成新输入 |
| 单文件 | 无跨文件/模块间契约检测 |
★ 最重要的一条:这个工具不能证明你的测试完美,只能证明你的测试有洞。
十一、一个邀请
如果你跑完发现自己项目的有效性很低,欢迎告诉我。
我更好奇的是:你觉得这个数有用吗? 如果没用,为什么?
—— 因为我不想做一个「看起来很酷但没人需要」的东西。
附:完整复现步骤
# 1. 装
npm install -g ai-test-check
# 2. 在你的项目里跑
cd your-project
ai-test-check ./src --max=20
# 3. 看结果
# 如果有效性 <60%,说明测试有洞
# 4. 看具体哪几处存活
# 输出会列出 文件名:行号 + 变异类型