The hardware and bandwidth for this mirror is donated by METANET, the Webhosting and Full Service-Cloud Provider.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]metanet.ch.
A mutator defines one kind of code change. When you pass a mutator to
muttest_plan(), muttest finds every matching pattern in
your source file and produces one mutant per match.
operator() β custom pairThe most flexible mutator. Replaces any token with any other token.
Use this when you need a specific swap not covered by the preset functions.
arithmetic_operators() β preset for arithmeticReturns a ready-made list of operator mutators covering common arithmetic mistakes:
| Original | Mutant |
|---|---|
+ |
- |
- |
+ |
* |
/ |
/ |
* |
^ |
* |
%% |
* |
%/% |
/ |
π‘ When to use: Any function that performs calculations β finance, statistics, data transformations. Arithmetic operator swaps can happen easily and go unnoticed.
An assertion that checks a property of the result (sign, order, non-negativity) rather than the value will be blind to arithmetic operator swaps. Both the original and the mutant satisfy the same directional constraint, so the mutant survives. Replacing the directional assertion with an exact-value assertion kills it.
mean_absolute_deviation computes the average distance
from a center point: mean(abs(x - center)). The subtraction
x - center is the step that arithmetic operator mutators
target.
test_that("mean absolute deviation is non-negative", {
expect_gte(mean_absolute_deviation(c(1, 3, 5), 3), 0)
})The test asserts that the result is non-negative
(expect_gte(..., 0)). This checks a property of the output
rather than its value.
Mutation testing output:
βΉ Mutation Testing
| K | S | E | T | % | Mutator | File
x | 0 | 1 | 0 | 1 | 0 | - β + | mad.R
ββ Results βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[ KILLED 0 | SURVIVED 1 | ERRORS 0 | TOTAL 1 | SCORE 0.0% ]
- β + survives. Changing x - center to
x + center gives a different number, but
abs(x + center) is also non-negative for all inputs. For
x = c(1,3,5) and center = 3:
abs(c(-2, 0, 2)) β mean = 4/3 β 1.33abs(c(4, 6, 8)) β mean = 6Both are β₯ 0. The assertion cannot distinguish them.
This pattern is common in LLM-generated tests that check what is obviously true about the result (it is positive, it is numeric) rather than what is specifically correct.
test_that("mean absolute deviation equals average distance from center", {
expect_equal(mean_absolute_deviation(c(1, 3, 5), 3), 4 / 3)
})Replace the directional assertion with expect_equal and
a value computed by hand. Now the mutant returns 6, which is not 4/3,
and the test fails.
After the fix:
βΉ Mutation Testing
| K | S | E | T | % | Mutator | File
β | 1 | 0 | 0 | 1 | 100 | - β + | mad.R
ββ Results βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[ KILLED 1 | SURVIVED 0 | ERRORS 0 | TOTAL 1 | SCORE 100.0% ]
When an arithmetic mutant survives, replace directional assertions (
expect_gt,expect_gte) with exact-value assertions (expect_equal). Compute the expected value by hand and hard-code it.
comparison_operators() β preset for comparisonsCovers boundary and direction mistakes in comparison expressions:
| Original | Mutant |
|---|---|
< |
> |
> |
< |
<= |
>= |
>= |
<= |
== |
!= |
!= |
== |
< |
<= |
> |
>= |
π‘ When to use: Functions with threshold logic, range checks, or filter conditions. Off-by-one and direction errors are easy to introduce and hard to catch with weak tests.
A test suite that checks only βclearly aboveβ and βclearly belowβ cases will let a boundary-shift mutant survive. The surviving mutant names the exact input your tests have never exercised. Adding a test at that precise boundary value kills it.
shipping_cost returns a flat rate based on whether the
package is over or under 5 kg. The strict > operator
means a 5 kg package pays the lower rate; >= would
charge it the higher rate.
test_that("heavy packages cost more than light ones", {
expect_gt(shipping_cost(10), shipping_cost(2))
})The test confirms that a heavy package costs more than a light one. It passes inputs of 10 and 2 kg β both clearly on opposite sides of the boundary β so it never exercises the value 5 itself.
Mutation testing output:
βΉ Mutation Testing
| K | S | E | T | % | Mutator | File
β | 1 | 0 | 0 | 1 | 100 | > β < | shipping.R
x | 1 | 1 | 0 | 2 | 50 | > β >= | shipping.R
ββ Results βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[ KILLED 1 | SURVIVED 1 | ERRORS 0 | TOTAL 2 | SCORE 50.0% ]
> β >= survives. Changing
weight_kg > 5 to weight_kg >= 5 only
affects input weight_kg = 5 exactly. For inputs 10 and 2,
the function returns identical results under both operators, so the test
cannot tell the operators apart.
In production, >= 5 instead of > 5
would route 5 kg shipments to the expensive tier and no test would catch
the regression.
test_that("heavy packages cost more than light ones", {
expect_gt(shipping_cost(10), shipping_cost(2))
})
test_that("5kg falls into the lower-cost tier", {
expect_equal(shipping_cost(5), 5.00)
})Add a test that passes the exact boundary value 5. With
> 5, the condition is FALSE and the
function returns 5.00. With >= 5, it is
TRUE and returns 15.00. This difference kills
the mutant.
After the fix:
βΉ Mutation Testing
| K | S | E | T | % | Mutator | File
β | 1 | 0 | 0 | 1 | 100 | > β < | shipping.R
β | 2 | 0 | 0 | 2 | 100 | > β >= | shipping.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.
logical_operators() β preset for logical operators| Original | Mutant |
|---|---|
&& |
\|\| |
\|\| |
&& |
& |
\| |
\| |
& |
π‘ When to use: Functions with compound conditions
(if (a && b)). Swapping && for
|| is a classic logic bug that coverage cannot detect.
|| vs
&&When test inputs are symmetric β both arguments true, or both
arguments false β || and && produce
identical results. A test suite built entirely from symmetric inputs
cannot distinguish the two operators, so || β &&
survives. Adding a test with one argument true and the other false
reveals the difference and kills the mutant.
can_access grants access when either
is_admin or is_owner is true, using
||. Swapping to && would require
both flags to be true β a fundamentally different access
policy.
test_that("access control works", {
expect_true(can_access(TRUE, TRUE))
expect_false(can_access(FALSE, FALSE))
})The tests cover only two cases: both TRUE (should allow
access) and both FALSE (should deny access). These are the
βhappy pathβ and the βall-invalidβ case β typical of tests written for
simple scenarios without thinking about individual flag variation.
Mutation testing output:
βΉ Mutation Testing
| K | S | E | T | % | Mutator | File
x | 0 | 1 | 0 | 1 | 0 | || β && | access.R
ββ Results βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[ KILLED 0 | SURVIVED 1 | ERRORS 0 | TOTAL 1 | SCORE 0.0% ]
|| β && survives for both test cases:
can_access(TRUE, TRUE):
TRUE && TRUE = TRUE β still passes
expect_truecan_access(FALSE, FALSE):
FALSE && FALSE = FALSE β still passes
expect_falseThe logical distinction between || and
&& only appears when the operands
disagree. No test ever passes (FALSE, TRUE) or
(TRUE, FALSE), so the function behaves identically for both
operators across the entire test suite.
test_that("access control works", {
expect_true(can_access(TRUE, TRUE))
expect_false(can_access(FALSE, FALSE))
})
test_that("owner-only access is granted", {
expect_true(can_access(FALSE, TRUE))
})
test_that("admin-only access is granted", {
expect_true(can_access(TRUE, FALSE))
})Add tests with asymmetric inputs β one flag true and the other false.
can_access(FALSE, TRUE) returns TRUE with
|| but FALSE with &&.
This difference kills the mutant.
After the fix:
βΉ Mutation Testing
| K | S | E | T | % | Mutator | File
β | 1 | 0 | 0 | 1 | 100 | || β && | access.R
ββ Results βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[ KILLED 1 | SURVIVED 0 | ERRORS 0 | TOTAL 1 | SCORE 100.0% ]
When a logical operator mutant survives, add tests with asymmetric inputs (one flag true, the other false). These inputs reveal the difference between
&&(requires both) and||(requires either).
boolean_literal() β flip TRUE/FALSEπ‘ When to use: Functions with hard-coded boolean
flags or default arguments like na.rm = TRUE,
stringsAsFactors = FALSE. Flipping the default reveals
whether tests exercise both states.
numeric_increment() β add 1 to every numeric
literalnumeric_decrement() β subtract 1 from every numeric
literalπ‘ When to use: Functions where exact numeric constants matter β thresholds, window sizes, counts, index boundaries. Off-by-one errors in constants are common and often untested.
string_fill() β replace empty strings with
"mutant"π‘ When to use: string_empty() is
useful for functions that return or compare string values β it checks
whether your tests would notice a blank output.
string_fill() tests whether your code handles non-empty
strings where empty ones are expected.
negate_condition() β wrap condition in
!(...)remove_condition_negation() β strip leading
!remove_negation() β remove ! anywhereπ‘ When to use: Functions with guard clauses and
early returns. These mutators reveal whether your tests cover both
branches of a condition. A surviving negate_condition()
mutant means the test inputs never trigger the FALSE
branch.
call_name() β swap one function name for anothercall_name("any", "all") # any(x) β all(x)
call_name("min", "max") # min(x) β max(x)
call_name("sum", "prod") # sum(x) β prod(x)π‘ When to use: Functions that delegate to summary
or aggregation helpers. any vs all and
min vs max are among the easiest mistakes to
make and the hardest to spot in a review.
na_literal() β swap NA/NULL valuesna_literal("NA", "NULL") # NA β NULL
na_literal("NULL", "NA") # NULL β NA
na_literal("NA", "NA_real_") # NA β NA_real_
na_literal("NA_real_", "NA") # NA_real_ β NAπ‘ When to use: Functions that accept or return
NA or NULL, especially any code with
is.na(), is.null(), or na.rm
handling. Swapping NA for NULL (and vice
versa) reveals whether callers distinguish between βvalue is missingβ
and βvalue is absentβ β two distinct concepts that R treats very
differently.
Swapping between typed NAs (NA_real_,
NA_integer_, NA_character_) and plain
NA checks whether type-sensitive downstream code
(e.g.Β vapply, dplyr::mutate) is covered.
replace_return_value() β replace explicit return
valuesreplace_return_value() # return(x) β return(NULL)
replace_return_value("NA") # return(x) β return(NA)π‘ When to use: Any function with explicit
return() calls. Tests that only check that a function
runs without error or returns something will not kill
these mutants β only tests that assert the specific value returned
will.
A surviving replace_return_value() mutant means the
caller never checks what came back from that branch. This is especially
common in functions with multiple early-exit return() paths
where only the happy path is tested.
Only explicit return(expr) calls are targeted; implicit
returns (the last expression of a function body) are not affected.
index_increment() β shift subscript indices up by
oneindex_decrement() β shift subscript indices down by
oneπ‘ When to use: Functions that index into vectors or lists by position or by a computed variable. Off-by-one errors in indexing are among the most common silent bugs in R β they produce a different element rather than an error, so they pass all tests unless a specific element value is asserted.
Only simple indices are mutated: plain identifiers
(x[i]) and numeric literals (x[1],
x[1L]). Complex expressions like x[a + b] or
x[seq_len(n)] are left untouched, keeping the mutant count
focused and the signal-to-noise ratio high.
Both single-bracket ([) and double-bracket
([[) indexing are covered.
If you are unsure where to begin, start here:
p <- muttest_plan(
source_files = "R/my_file.R",
mutators = c(
arithmetic_operators(),
comparison_operators(),
logical_operators(),
condition_mutations(),
numeric_literals(),
list(remove_negation())
)
)Look at the survivors, then layer in boolean_literals(),
na_literals(), string_literals(), or
index_mutations() for the specific patterns in your
code.
The better you know your codebase, the more you can target mutators
to the patterns that are most likely to harbor bugs. For example, if
your code has a lot of if statements but no numeric
constants, condition mutators will be more effective than numeric
ones.
Youβll figure it out as you go.
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.