
Running muttest in CI
ci-integration.RmdRunning 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
muttestlocally; 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:
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 diffto 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_filestightly — 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.