This guide walks you through your first mutation test run, from setup to interpreting results.
muttest works with R packages with testthat
tests out of the box. If you’re not using a package structure, or not
using testthat, see ?TestStrategy for
configuration options.
Start small. Choose one file from R/ that contains
meaningful logic — branching, arithmetic, comparisons. Avoid files that
are mostly glue code or just call other functions.
A test plan describes what to mutate and which mutations to apply:
library(muttest)
plan <- muttest_plan(
source_files = "R/is_adult.R",
mutators = comparison_operators()
)comparison_operators() is a preset that generates
mutants by swapping each comparison operator for related alternatives.
For >= it produces two mutants: >= →
> and >= → <=.
Each column in the progress table means:
| Column | Meaning |
|---|---|
| K | Killed — mutants your tests caught |
| S | Survived — mutants your tests missed |
| E | Errors — mutants that caused unexpected errors |
| T | Total mutants for this mutator/file combination |
| % | Mutation score for this row |
The mutation score is
Killed / Total × 100%. A ✔ row means at least
one mutant was killed; an x row means all mutants
survived.
Here is a complete example showing a weak test, the live output, and the fix:
is_adult — Missing Boundary ValueA test suite that checks only values clearly on one side of a threshold will let boundary-shift comparison mutants survive. The surviving mutant names the exact input your tests have never exercised. Adding a test at that precise boundary kills it.
is_adult returns TRUE when age is 18 or
older, using >=. Swapping to > would
incorrectly classify an 18-year-old as a minor.
test_that("is_adult returns TRUE for adults", {
expect_true(is_adult(25))
})
test_that("is_adult returns FALSE for minors", {
expect_false(is_adult(10))
})The tests check age 25 (clearly adult) and age 10 (clearly minor).
Both inputs return the same result whether the operator is
>= or >, so the tests cannot tell the
operators apart.
Mutation testing output:
ℹ Mutation Testing
| K | S | E | T | % | Mutator | File
✔ | 1 | 0 | 0 | 1 | 100 | >= → <= | is_adult.R
x | 1 | 1 | 0 | 2 | 50 | >= → > | is_adult.R
── Results ─────────────────────────────────────────────────────────────────────
[ KILLED 1 | SURVIVED 1 | ERRORS 0 | TOTAL 2 | SCORE 50.0% ]
>= → > survives. Changing
age >= 18 to age > 18 only affects one
input — age = 18 exactly. The tests never pass 18, so the
function behaves identically for 25 and 10 under both operators.
test_that("is_adult returns TRUE for adults", {
expect_true(is_adult(25))
})
test_that("is_adult returns FALSE for minors", {
expect_false(is_adult(10))
})
test_that("is_adult returns TRUE at the boundary age", {
expect_true(is_adult(18))
})Add a test at the boundary value 18. With >= 18, the
condition is TRUE and the function returns
TRUE. With > 18, it is FALSE
and returns FALSE. This difference kills the mutant.
After the fix:
ℹ Mutation Testing
| K | S | E | T | % | Mutator | File
✔ | 1 | 0 | 0 | 1 | 100 | >= → <= | is_adult.R
✔ | 2 | 0 | 0 | 2 | 100 | >= → > | is_adult.R
── Results ─────────────────────────────────────────────────────────────────────
[ KILLED 2 | SURVIVED 0 | ERRORS 0 | TOTAL 2 | SCORE 100.0% ]
When a comparison mutant survives, find the boundary value implied by the operator and add a test that passes exactly that value.
Start with one file. Aim for a meaningful score improvement each iteration rather than chasing 100% immediately. A score of 80%+ on critical business logic can be a reasonable target to start from.