Running muttest in CI

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

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:

Skip:

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