5 分钟验证你的测试是不是假的:变异测试入门(附 8 个真实项目数据)

6 阅读7分钟

开头:先给你一个 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变异数被杀有效性
1browserify/events1.4k1010100%
2sindresorhus/slugify2.7k10660%
3jonschlinkert/is-number28610220%
4browserify/sha.js2981000%
5crypto-browserify/createHash54800%
6chalk/chalk23.3k1000%
7ljharb/object.assign1061000%
8browserify/parse-asn1201000%
平均≈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. 看具体哪几处存活
# 输出会列出 文件名:行号 + 变异类型