The 100% Coverage Paradox — or Why Your Tests Might Be Lying
\n\nYou wrote perfect code in Rust. The compiler is happy, clippy is silent, tests pass with a green wave, and the coverage report proudly states: 100% of lines are covered. Can you breathe a sigh of relief? No. Mutation testing in Rust is precisely the technique that will show you that your tests might be protecting the air, not the logic.
\n\nImagine: you accidentally change + to - in a critical function. Are the tests still green? Congratulations, you've found a mutant — a defect that lives in your code but goes unnoticed by tests. In this article, we'll break down how mutation testing in Rust helps find such blind spots and implement it in a real project.
What is Mutation Testing (and Why Rust is Ideal for It)
\n\nMutation testing is a method for evaluating test quality where small artificial changes (mutations) are introduced into the source code, and then the tests are run. If the tests don't fail on the mutated code, the mutant has survived. The more surviving mutants, the worse your tests are.
\n\nRust is a language that itself catches entire classes of errors at compile time: race conditions, null pointers, use-after-free. But mutation testing in Rust targets precisely logical errors — what the compiler doesn't see. And this makes Rust an ideal platform: you're already protected from 70% of typical bugs, and mutation testing finishes off the remaining 30%.
\n\nWhat a Mutation Looks Like in Rust Code
\n\nTypical mutations include:
\n- \n
- Operator changes:
a + b→a - b\n - Condition inversion:
if x > 5→if x < 5\n - Constant replacement:
let n = 42→let n = 0\n - Removing function calls:
process(data)→ empty call \n - Changing iteration bounds:
0..n→0..=n\n
If after such a mutation at least one test does not fail — it means you wasted time writing that test. It doesn't check what it's supposed to.
\n\nTools for Mutation Testing in Rust: cargo-mutants
\n\nToday, the main tool for mutation testing in Rust is cargo-mutants. It's mature, actively maintained, and provides readable reports. Installation is simple:
cargo install cargo-mutants\n\n\nAfter that, just run:
\n\ncargo mutants\n\n\nAnd the tool will automatically find all mutations in your code, run the tests, and output an HTML report with color indicators: green — killed mutants (tests worked), red — surviving mutants (problem).
\n\nAlternatives on the Radar
\n- \n
- mutagen — older, but still a working tool. Fewer mutations, faster on small projects. \n
- cargo-insta — not exactly mutation testing, but close: helps visually check changes in test snapshots. \n
For serious work, I recommend cargo-mutants — it offers the best balance between speed and depth of analysis.
Practical Example: Testing a Discount Calculator
\n\nLet's say we have a module for calculating discounts in an online store in Rust:
\n\n// src/discount.rs\n\npub fn calculate_discount(price: f64, customer_age: u32) -> f64 {\n let mut discount = 0.0;\n\n // 10% discount for seniors (over 65)\n if customer_age > 65 {\n discount += 0.1;\n }\n\n // Additional 5% discount for purchases over 1000 rubles\n if price > 1000.0 {\n discount += 0.05;\n }\n\n // Maximum discount no more than 30%\n if discount > 0.30 {\n discount = 0.30;\n