一、先看效果
配好之后,每个 PR 会多一个检查:
✅ Test Effectiveness / Check test effectiveness (23s)
变异总数: 20
被杀: 14(测试抓到了)
存活: 6(测试没抓到)
★ 测试有效性: 70%
存活的变异按类型分组:
边界条件(4 处)⇒ 补「等于/不等于」的情况
逻辑组合(2 处)⇒ 补「只满足一个条件」的情况
如果有效性 <50%,PR 会变红,不能合并。
二、手把手:5 分钟配好
# 1. 在你的项目根目录创建目录
mkdir -p .github/workflows
# 2. 创建文件
touch .github/workflows/mutation.yml
# 3. 粘贴下面的配置
# 4. 提交
git add .github/workflows/mutation.yml
git commit -m "ci: add test effectiveness check"
git push
★ 配好后的效果:下一个 PR 会自动多一个检查。
三、完整配置
把下面这个文件放到 .github/workflows/mutation.yml:
name: Test Effectiveness
on:
pull_request:
branches: [main, master]
jobs:
mutation:
name: Check test effectiveness
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Run mutation test
id: mutation
continue-on-error: true
run: npx ai-test-check ./src --max=20 | tee mutation-report.txt
- name: Show report
if: always()
run: cat mutation-report.txt
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: mutation-report
path: mutation-report.txt
retention-days: 7
- name: Fail if effectiveness < 50%
if: steps.mutation.outcome == 'failure'
run: |
echo "::error::Test effectiveness is below 50%. Add more edge-case tests."
exit 1
四、★ 三个关键设计
4.1 为什么用 continue-on-error: true
- name: Run mutation test
id: mutation
continue-on-error: true # ★ 不立刻失败
run: npx ai-test-check ./src --max=20 | tee mutation-report.txt
原因:
如果直接失败 ⇒ 后面的「Show report」和「Upload report」不会执行
⇒ 开发者看不到报告
⇒ 不知道该补什么
用 continue-on-error ⇒ 报告照常生成
⇒ 最后一步才决定是否失败
⇒ 开发者能看到完整报告
4.2 为什么用 tee
npx ai-test-check ./src --max=20 | tee mutation-report.txt
原因:
① 输出到终端(开发者能直接看到)
② 同时保存到文件(上传 artifact 用)
★ tee = 一分为二
4.3 为什么最后一步才失败
- name: Fail if effectiveness < 50%
if: steps.mutation.outcome == 'failure'
run: |
echo "::error::Test effectiveness is below 50%."
exit 1
原因:
① 前面的步骤用 continue-on-error 兜住
② 报告已生成、已上传
③ 最后才决定 PR 红不红
★ 顺序很重要:先生成报告,再决定失败
五、★ 怎么调阈值
5.1 按项目阶段调
| 项目阶段 | 建议阈值 |
|---|---|
| 刚引入 | 30% |
| 有基础 | 50%(默认) |
| 严格要求 | 70% |
5.2 自定义阈值
ai-test-check 的退出码:
0 = 有效性 >= 50%
1 = 有效性 < 50%
如果你想用别的阈值,可以解析输出:
- name: Run mutation test
id: mutation
continue-on-error: true
run: |
npx ai-test-check ./src --max=20 | tee mutation-report.txt
RATE=$(grep -oP '测试有效性: \K\d+' mutation-report.txt || echo 0)
echo "rate=$RATE" >> $GITHUB_OUTPUT
if [ "$RATE" -lt 70 ]; then exit 1; fi
★ 但我不建议一开始就用高阈值:
新项目:30%
⇒ 先让人习惯「有这个检查」
⇒ 再慢慢提高
直接 70%:
⇒ 大部分 PR 都红
⇒ 团队会关掉这个检查
六、★ 只检查改动的文件(性能优化)
问题:全量跑变异测试可能很慢(20 个变异 × 每个跑一次测试)
优化:只测 PR 改动的文件
- name: Get changed files
id: changed
run: |
git diff --name-only origin/${{ github.base_ref }}...HEAD > changed.txt
- name: Run mutation test on changed files
continue-on-error: true
run: |
FILES=$(grep '\.js$' changed.txt | tr '\n' ',' | sed 's/,$//')
if [ -n "$FILES" ]; then
npx ai-test-check ./src --file="$FILES" --max=10
fi
★ 但注意:
只测改动文件有风险:
⇒ 改 A 文件可能影响 B 文件的测试
⇒ 但变异测试是「改源码看测试红不红」
⇒ 所以只测改动文件通常够用
★ 建议:PR 时只测改动,合并前跑全量
七、★ 常见报错
| 报错 | 原因 | 解决 |
|---|---|---|
npx: command not found | Node 没装 | 加 actions/setup-node@v4 |
❌ 基线测试就是红的 | 项目测试本来就失败 | 先修测试 |
可变异点: 0 个 | 源码里没有运算符 | 检查 ./src 路径 |
⚠ N 个文件解析失败 | 有些文件是 ES5/TS | 正常,跳过 |
| Action 超时 | 变异太多 | 减少 --max |
| 报告为空 | tee 没写文件 | 检查路径 |
八、★ 完整工作流(含缓存)
name: Test Effectiveness
on:
pull_request:
branches: [main, master]
jobs:
mutation:
name: Check test effectiveness
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm' # ★ 缓存 npm 依赖
- run: npm ci
- name: Run mutation test
id: mutation
continue-on-error: true
run: npx ai-test-check ./src --max=20 | tee mutation-report.txt
- name: Show report
if: always()
run: cat mutation-report.txt
- uses: actions/upload-artifact@v4
if: always()
with:
name: mutation-report
path: mutation-report.txt
retention-days: 7
- name: Fail if effectiveness < 50%
if: steps.mutation.outcome == 'failure'
run: |
echo "::error::Test effectiveness is below 50%."
exit 1
★ cache: 'npm' 是关键:
第一次跑:下载依赖(慢)
后续跑:从缓存恢复(快)
★ 能省 30-60 秒
九、★ 和覆盖率 CI 的区别
| 检查 | 回答什么 | 什么时候失败 |
|---|---|---|
| Coverage | 代码被执行了吗 | 覆盖率 < 阈值 |
| Mutation | 测试能抓到 bug 吗 | 有效性 < 阈值 |
★ 两者互补:
Coverage 100% + 有效性 40%
⇒ 每行都跑了,但一半改动测试看不见
⇒ 这是「假绿」
Coverage 60% + 有效性 90%
⇒ 有些代码没测,但测了的部分是真的
⇒ 这比上面健康
★ 建议两个都配:
- name: Coverage
run: npm test -- --coverage
- name: Mutation
run: npx ai-test-check ./src --max=20
十、★ 一个反直觉的建议
不要一开始就卡 PR。
阶段 1(第 1 周):只报告,不失败
⇒ 让人看到数字
⇒ 讨论「这个数字合理吗」
阶段 2(第 2-4 周):卡但阈值低
⇒ 阈值 30%
⇒ 大部分 PR 能过
阶段 3(第 2 个月):逐步提高
⇒ 30% → 40% → 50%
★ 一步到位卡 70% 会让团队直接关掉检查
十一、诚实边界
| 限制 | 说明 |
|---|---|
| 只支持 JS/CJS | 不支持 TS/Python |
| 变异类型有限 | 13 种运算符 + 5 类字面量 |
| 慢 | 20 个变异 × 每个跑一次测试 = 20-40 秒 |
| 单文件 | 无跨文件契约检测 |
| 「100%」不等于完美 | 只是「这 N 个变异都被抓到了」 |
★ 最重要的一条:这个检查不能替代其他测试,只是补上一个盲区。
十二、完整示例
GitHub 仓库:github.com/moqinghua20…
工作流文件:.github/workflows/mutation.yml
两个可跑的 demo:
examples/calculator/— 简单示例(有效性 44%)examples/full/— 完整示例(修复前 42%,修复后 100%)
十三、一句话
把「测试有效性」加进 CI,只需 40 行 YAML。关键是:①用 continue-on-error 先出报告再决定失败 ②用 tee 同时输出到终端和文件 ③阈值从 30% 起步,别一步到位。