Mutation Testing: How Does it Work in Rust?

featured-image

I've been a big fan of Mutation Testing since I discovered PIT. As I dive deeper into Rust, I wanted to check the state of mutation testing in Rust.

I've been a big fan of Mutation Testing since I discovered PIT. As I dive deeper into Rust, I wanted to check the state of mutation testing in Rust.Starting with cargo-mutantsI found two crates for mutation testing in Rust:cargo-mutantsand mutagenmutagen hasn't been maintained for three years, while cargo-mutants is still under active development.

I've ported the sample code from my previous Java code to Rust:struct LowPassPredicate { threshold: i32,}impl LowPassPredicate { pub fn new(threshold: i32) -> Self { LowPassPredicate { threshold } } pub fn test(&self, value: i32) -> bool { value < self.threshold }}#[cfg(test)]mod tests { use super::*; #[test] fn should_return_true_when_under_limit() { let low_pass_predicate = LowPassPredicate::new(5); assert_eq!(low_pass_predicate.test(4), true); } #[test] fn should_return_false_when_above_limit() { let low_pass_predicate = LowPassPredicate::new(5); assert_eq!(low_pass_predicate.



test(6), false); }}Using cargo-mutants is a two-step process:Install it, cargo install --locked cargo-mutantsUse it, cargo mutantsFound 4 mutants to testok Unmutated baseline in 0.1s build + 0.3s test INFO Auto-set test timeout to 20s4 mutants tested in 1s: 4 caughtI expected a mutant to survive, as I didn't test the boundary when the test value equals the limit.

Strangely enough, cargo-mutants didn't detect it.Finding and Fixing the IssueI investigated the source code and found the place where it mutates operators:// We try replacing logical ops with == and !=, which are effectively// XNOR and XOR when applied to booleans. However, they're often unviable// because they require parenthesis for disambiguation in many expressions.

BinOp::Eq(_) => vec![quote! { != }],BinOp::Ne(_) => vec![quote! { == }],BinOp::And(_) => vec![quote! { || }],BinOp::Or(_) => vec![quote! { && }],BinOp::Lt(_) => vec![quote! { == }, quote! {>}],BinOp::Gt(_) => vec![quote! { == }, quote! { vec![quote! {>}],BinOp::Ge(_) => vec![quote! { vec![quote! {-}, quote! {*}],Indeed, is changed to == and >, but not to . I forked the repo and updated the code accordingly:BinOp::Lt(_) => vec![quote! { == }, quote! {>}, quote!{ vec![quote! { == }, quote! { }],I installed the new forked version:cargo install --git https://github.com/nfrankel/cargo-mutants.

git --lockedI reran the command:cargo mutantsThe output is the following:Found 5 mutants to testok Unmutated baseline in 0.1s build + 0.3s test INFO Auto-set test timeout to 20sMISSED src/lib.

rs:11:15: replace < with You can find the same information in the missed.txt file. I thought I fixed it and was ready to make a Pull Request to the cargo-mutants repo.

I just needed to add the test at the boundary:#[test]fn should_return_false_when_equals_limit() { let low_pass_predicate = LowPassPredicate::new(5); assert_eq!(low_pass_predicate.test(5), false);}cargo testrunning 3 teststest tests::should_return_false_when_above_limit ..

. oktest tests::should_return_false_when_equals_limit ..

. oktest tests::should_return_true_when_under_limit ..

. oktest result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.

00scargo mutantsAnd all mutants are killed!Found 5 mutants to testok Unmutated baseline in 0.1s build + 0.2s test INFO Auto-set test timeout to 20s5 mutants tested in 2s: 5 caughtConclusionNot many blog posts end with a Pull Request, but this one does:https://github.

com/sourcefrog/cargo-mutants/pull/501?embedable=trueUnfortunately, I couldn't manage to make the tests pass; fortunately, the repository maintainer helped me–a lot. The Pull Request is merged: enjoy this slight improvement.I learned more about cargo-mutants and could improve the code in the process.

To go further:Mutation testingWelcome to cargo-mutantsGitHub cargo-mutantsOriginally published at A Java Geek on March 30th, 2025.