把「测试有效性」加进 CI:GitHub Actions 配置指南

1 阅读5分钟

一、先看效果

配好之后,每个 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 foundNode 没装加 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% 起步,别一步到位。