一、先看结果
| 项 | 修复前 | 修复后 |
|---|---|---|
| 测试数量 | 5 个 | 20 个 |
| 变异点 | 18 个 | 18 个 |
| 被杀 | 7 个 | 18 个 |
| 存活 | 11 个 | 0 个 |
| 有效性 | 39% | 100% |
同一个项目,同一份源码,只改测试。
二、修复前:5 个测试全绿
源码是一个简单的计算器:
function isAdult(age) {
return age >= 18;
}
function discount(total) {
if (total >= 100) {
return total * 0.9;
}
return total;
}
function canVote(age, isCitizen) {
return age >= 18 && isCitizen === true;
}
function isWeekday(day) {
return day >= 1 && day <= 5;
}
function getStatus(code) {
if (code === 'active') { return 'ok'; }
return 'unknown';
}
测试:
const assert = require('assert');
assert.strictEqual(isAdult(20), true);
assert.strictEqual(discount(200), 180);
assert.strictEqual(canVote(20, true), true);
assert.strictEqual(isWeekday(3), true);
assert.strictEqual(getStatus('active'), 'ok');
console.log('✅ 5 个测试全部通过');
跑起来:全部通过。CI 绿。看起来没问题。
三、手把手:跑一遍看结果
# 1. 装(或用 npx,不用装)
npx ai-test-check . --all
# 2. 看输出
# 如果有效性 <50%,说明测试有洞
预期输出(就是这个 demo 的真实结果):
可变异点: 18 个,本次抽 18 个(全量 ✅ 结果稳定)
...
★ 测试有效性: 39%
四、但 ai-test-check 说:39%
npx ai-test-check . --all
输出:
可变异点: 18 个,本次抽 18 个(全量 ✅ 结果稳定)
[1/18] calc.js 数字边界 ❌ 存活(测试没抓到)
[2/18] calc.js === → !== ✅ 被杀(测试抓到了)
[3/18] calc.js >= → > ❌ 存活(测试没抓到)
[4/18] calc.js && → || ❌ 存活(测试没抓到)
[5/18] calc.js 状态字符串 ✅ 被杀(测试抓到了)
[6/18] calc.js >= → > ❌ 存活(测试没抓到)
[7/18] calc.js === → !== ✅ 被杀(测试抓到了)
[8/18] calc.js && → || ❌ 存活(测试没抓到)
[9/18] calc.js 数字边界 ❌ 存活(测试没抓到)
[10/18] calc.js <= → < ❌ 存活(测试没抓到)
[11/18] calc.js >= → > ❌ 存活(测试没抓到)
[12/18] calc.js 条件取反 ✅ 被杀(测试抓到了)
[13/18] calc.js 数字边界 ❌ 存活(测试没抓到)
[14/18] calc.js 条件取反 ✅ 被杀(测试抓到了)
[15/18] calc.js true ↔ false ✅ 被杀(测试抓到了)
[16/18] calc.js >= → > ❌ 存活(测试没抓到)
[17/18] calc.js 数字边界 ✅ 被杀(测试抓到了)
[18/18] calc.js 数字边界 ❌ 存活(测试没抓到)
★ 测试有效性: 39%
★ 18 个变异,只有 7 个被抓到。
五、★ 关键:分组报告告诉你该补什么
存活的变异按类型分组:
边界条件(5 处)⇒ 补「等于/不等于」的情况
数字边界(4 处)⇒ 补「刚好等于/差一」的情况
逻辑组合(2 处)⇒ 补「只满足一个条件」的情况
这比「有效性 39%」有用得多 —— 它直接告诉你:
① 5 处边界没测 ⇒ 补 >= 和 <= 的边界
② 4 处数字边界 ⇒ 补「刚好差一」的值
③ 2 处逻辑组合 ⇒ 补「只满足一个条件」
六、按报告修复
修复 1:边界条件(5 处)
// isAdult: >= 18 的边界
assert.strictEqual(isAdult(18), true);
assert.strictEqual(isAdult(17), false);
// discount: >= 100 的边界
assert.strictEqual(discount(100), 90);
assert.strictEqual(discount(99), 99);
// canVote: >= 18 的边界
assert.strictEqual(canVote(18, true), true);
assert.strictEqual(canVote(17, true), false);
// isWeekday: >= 1 和 <= 5 的边界
assert.strictEqual(isWeekday(1), true);
assert.strictEqual(isWeekday(5), true);
assert.strictEqual(isWeekday(0), false);
assert.strictEqual(isWeekday(6), false);
修复 2:数字边界(4 处)
assert.strictEqual(isAdult(19), true); // 抓 18 → 19
assert.strictEqual(discount(101), 90.9); // 抓 100 → 101
assert.strictEqual(isWeekday(2), true); // 抓 1 → 2
修复 3:逻辑组合(2 处)
assert.strictEqual(canVote(20, false), false); // 年龄够,但不是公民
assert.strictEqual(canVote(17, false), false); // 两个都不满足
修复 4:状态字符串(顺手补的)
assert.strictEqual(getStatus('inactive'), 'unknown');
assert.strictEqual(getStatus('other'), 'unknown');
修复 5:条件取反(顺手补的)
assert.strictEqual(discount(0), 0);
七、修复后:100%
$ node test-fixed.js
✅ 20 个测试全部通过
$ npx ai-test-check . --all --test-cmd="node test-fixed.js"
[1/18] calc.js 数字边界 ✅ 被杀
[2/18] calc.js === → !== ✅ 被杀
[3/18] calc.js >= → > ✅ 被杀
[4/18] calc.js && → || ✅ 被杀
[5/18] calc.js 状态字符串 ✅ 被杀
[6/18] calc.js >= → > ✅ 被杀
[7/18] calc.js === → !== ✅ 被杀
[8/18] calc.js && → || ✅ 被杀
[9/18] calc.js 数字边界 ✅ 被杀
[10/18] calc.js <= → < ✅ 被杀
[11/18] calc.js >= → > ✅ 被杀
[12/18] calc.js 条件取反 ✅ 被杀
[13/18] calc.js 数字边界 ✅ 被杀
[14/18] calc.js 条件取反 ✅ 被杀
[15/18] calc.js true ↔ false ✅ 被杀
[16/18] calc.js >= → > ✅ 被杀
[17/18] calc.js 数字边界 ✅ 被杀
[18/18] calc.js 数字边界 ✅ 被杀
★ 测试有效性: 100%
★ 18 个变异,18 个被抓,0 个存活。
八、★ 这个对比说明什么
8.1 「测试全绿」和「测试有效」是两件事
修复前:
✅ 5 个测试全绿
✅ CI 通过
✅ 看起来没问题
❌ 但有效性只有 39%
★ 你改坏 10 处代码,测试只能抓到 3.9 处
8.2 分组报告是可执行的
不用自己猜「该补什么测试」
⇒ 报告说「边界条件 5 处」
⇒ 就补边界测试
⇒ 报告说「逻辑组合 2 处」
⇒ 就补逻辑组合测试
★ 修完从 39% → 100%
8.3 这个数字可以量化
39% ⇒ 改坏 10 处,抓 3.9 处
100% ⇒ 改坏 10 处,全抓到
★ 这是可测量的,不是感觉
九、常见报错
| 报错 | 原因 | 解决 |
|---|---|---|
❌ 基线测试就是红的 | 你的测试本来就失败 | 先让测试全绿 |
❌ 找不到测试命令 | 没有 scripts.test | 用 --test-cmd="node test.js" |
可变异点: 0 个 | 源码里没有运算符/字面量 | 换一个有逻辑的目录 |
⚠ N 个文件解析失败 | 有些文件是 ES5 或 TS | 正常,跳过(会报告) |
| 结果每次不一样 | 抽样波动 | 用 --all 跑全量 |
十、怎么复现
git clone https://github.com/moqinghua2020/ai-test-check
cd ai-test-check/examples/full
# 修复前
node test.js
npx ai-test-check . --all
# ⇒ 39%
# 修复后
node test-fixed.js
npx ai-test-check . --all --test-cmd="node test-fixed.js"
# ⇒ 100%
# 对比两个测试文件
diff test.js test-fixed.js
完整记录:examples/full/BEFORE-AFTER.md
十一、诚实边界
| 限制 | 说明 |
|---|---|
| 这是 demo 项目 | 不是真实项目,但源码和测试都是真实写法 |
| 只测 JS | 不支持 TS/Python |
| 变异类型有限 | 5 类(运算符/布尔/数字/状态/条件取反) |
| 100% 不等于完美 | 只是「这 18 个变异都被抓到了」 |
★ 最重要的一条:「100%」不代表「测试完美」,只代表「这个工具抽的 18 个变异都被抓到了」。
十二、一句话
5 个测试全绿,但有效性只有 39%。按分组报告补 15 个边界测试后,有效性 100%。这就是「假绿」的具体样子,也是它能被修复的证据。