变异测试实战:5 个测试全绿但有效性只有 39%,我把它修到了 100%

2 阅读5分钟

一、先看结果

项修复前修复后
测试数量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%。这就是「假绿」的具体样子,也是它能被修复的证据。