Skip to contents

Running mutation tests in CI gives you a persistent, objective record of test quality that evolves alongside your codebase. This guide covers GitHub Actions setup, badge reporting, and practical tips for keeping CI runs fast.

Why run mutation tests in CI

  • Regression detection — a drop in mutation score on a PR signals that new code shipped with weak tests.
  • Visible accountability — a badge in the README makes test quality a first-class metric alongside coverage.
  • Automated baseline — no one has to remember to run muttest locally; the score is always up to date.

GitHub Actions: minimal workflow

Create .github/workflows/test-mutation.yaml:

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

name: Mutation Testing

jobs:
  mutation-test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: r-lib/actions/setup-r@v2
        with:
          use-public-rspm: true

      - uses: r-lib/actions/setup-r-dependencies@v2
        with:
          extra-packages: |
            jakubsob/muttest

      - name: Run mutation tests
        shell: Rscript {0}
        run: |
          plan <- muttest::muttest_plan(c(
            muttest::arithmetic_operators(),
            muttest::comparison_operators()
          ))
          muttest::muttest(plan)

Adjust source_files and mutators to match your package.

Mutation score badge

The muttest repository ships a badge workflow that writes a JSON endpoint to a badges branch and renders it as a shield. See the badge workflow in the muttest repository for the full implementation pattern.

The badge in your README looks like:

[![muttest](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/YOUR_ORG/YOUR_REPO/badges/badges/muttest.json)](https://github.com/YOUR_ORG/YOUR_REPO/actions/workflows/test-mutation.yaml)

Choosing which files to mutate in CI

Mutation testing multiplies your test runtime: running N mutants means running your test suite N times. Keep CI fast by being selective:

Mutate:

  • Files with complex branching or arithmetic logic
  • Critical domain code (validation, calculations, filters)
  • Recently changed files (use git diff to scope a PR run)

Skip:

  • Pure utility files with trivial logic
  • Auto-generated code
  • Files already tested by slow integration tests (run those separately)

A practical approach: define a narrow source_files list in your CI plan covering only the high-value files, and run the broader set locally or on a nightly schedule.

Failing the build on a low score

To enforce a minimum mutation score, parse the output or use R’s exit codes:

score <- muttest::muttest(plan)
if (score < 0.7) {
  message(sprintf("Mutation score %.1f%% is below threshold 70%%", score * 100))
  quit(status = 1)
}

Start with a threshold that reflects your current score, then tighten it over time as you improve your tests.

Performance tips

  • Scope source_files tightly — the fewer files, the fewer mutants, the faster the run.
  • Use fast unit tests — mutation testing re-runs your test suite for each mutant. If individual tests are slow, the total time scales badly. Prefer tests that mock external dependencies.
  • Run on PRs to changed files only — scope the mutation plan to files touched by the PR using git diff --name-only origin/main...HEAD.