Skip to main content

software_engineering/
test_effectiveness.rs

1//! # Test Coverage and Test Effectiveness
2//!
3//! **Test coverage** measures the percentage of code executed by a test
4//! suite — line, branch, or path coverage. It is cheap to compute and easy
5//! to visualize as a single percentage, which also makes it one of the most
6//! frequently gamed metrics in software engineering: coverage measures
7//! whether code executed during a test run, not whether the test actually
8//! checked that the code behaved correctly. **Mutation testing** answers
9//! that gap directly: it deliberately introduces small, artificial faults
10//! into the code and checks whether the test suite catches ("kills") them,
11//! and is coverage's necessary complement, not an optional extra.
12//!
13//! ## Formula
14//!
15//! ```text
16//! Test coverage %     = lines (or branches) covered / total lines × 100
17//! Mutation kill rate % = mutants killed / mutants total × 100
18//!
19//! covered / killed = execution or detection count
20//! total            = denominator (lines, branches, or mutants); zero is undefined
21//! ```
22//!
23//! ## Why it matters
24//!
25//! A coverage target with no effectiveness check is a textbook Goodhart's
26//! law setup: the number improves while genuine quality does not. A test
27//! suite with high line coverage but a low mutation-kill rate is executing
28//! code without meaningfully checking it — pairing the two is the single
29//! most effective guardrail against coverage-target gaming.
30//!
31//! ## Example
32//!
33//! The topic doc's enterprise example: a company-wide 95% coverage
34//! requirement, enforced as a hard CI gate, coexisted with a mutation-kill
35//! rate under 40% across much of the codebase — tests executed code without
36//! meaningfully asserting on its behaviour. The revised policy required an
37//! 80% mutation-kill-rate threshold for payment and authentication code.
38//!
39//! ```rust
40//! use software_engineering::test_effectiveness::{
41//!     mutation_kill_rate_percent, test_coverage_percent,
42//! };
43//!
44//! // A company-wide "95% coverage requirement" met at face value...
45//! let coverage = test_coverage_percent(95.0, 100.0);
46//! assert_eq!(coverage, Some(95.0));
47//!
48//! // ...while the mutation-kill rate is "under 40%" on the same code —
49//! // high coverage, weak verification.
50//! let kill_rate = mutation_kill_rate_percent(38.0, 100.0).unwrap();
51//! assert!(kill_rate < 40.0);
52//!
53//! // The revised policy's "80% kill-rate threshold" for critical code.
54//! let critical_kill_rate = mutation_kill_rate_percent(85.0, 100.0).unwrap();
55//! assert!(critical_kill_rate >= 80.0);
56//!
57//! // An empty denominator leaves both rates undefined.
58//! assert_eq!(test_coverage_percent(1.0, 0.0), None);
59//! assert_eq!(mutation_kill_rate_percent(1.0, 0.0), None);
60//! ```
61//!
62//! ## Pitfalls
63//!
64//! - **Treating coverage percentage as a direct quality verdict** — it
65//!   measures execution, not verification.
66//! - **Writing tests primarily to satisfy a coverage gate**, producing the
67//!   threshold-gaming pattern a coverage-only target invites.
68//! - **Disabling or deleting failing tests** instead of fixing the
69//!   underlying problem, which removes real protection while the reported
70//!   number barely moves.
71//! - **Applying a uniform coverage target regardless of code risk**, wasting
72//!   effort on low-risk code and under-investing in critical paths.
73//! - **Treating a coverage-mutation gap as evidence of nothing**: a large
74//!   gap between a high coverage number and a low mutation-kill rate is the
75//!   clearest sign that coverage alone is not telling you what you think.
76//!
77//! ## Sources
78//!
79//! - Chapter 4.2, Test coverage and test effectiveness.
80//! - Jia, Yue, and Mark Harman, "An Analysis and Survey of the Development
81//!   of Mutation Testing," *IEEE Transactions on Software Engineering*
82//!   (2011).
83//!
84//! Topic doc: software-engineering-metrics/locales/en-001/chapters/04-02-test-coverage-and-test-effectiveness.md
85
86/// Test coverage as a percentage: covered lines (or branches) / total × 100.
87///
88/// Coverage measures whether code executed during a test run, not whether
89/// the test meaningfully verified its behaviour. Use it to find untested
90/// code (a floor), not as a ceiling to maximize; pair it with
91/// [`mutation_kill_rate_percent`] to check that the covered code is actually
92/// being verified.
93///
94/// # Arguments
95///
96/// * `covered` — lines, branches, or paths executed by the test suite.
97/// * `total` — total lines, branches, or paths in the measured code.
98///
99/// # Returns
100///
101/// `Some(percentage)` (e.g. `95.0` for 95%), or `None` when `total` is zero
102/// (coverage undefined — there is no code to cover).
103///
104/// # Examples
105///
106/// ```rust
107/// use software_engineering::test_effectiveness::test_coverage_percent;
108///
109/// // A company-wide 95% coverage requirement, met at face value.
110/// assert_eq!(test_coverage_percent(95.0, 100.0), Some(95.0));
111/// assert_eq!(test_coverage_percent(1.0, 0.0), None);
112/// ```
113#[must_use]
114pub fn test_coverage_percent(covered: f64, total: f64) -> Option<f64> {
115    if total == 0.0 {
116        None
117    } else {
118        Some(covered / total * 100.0)
119    }
120}
121
122/// Mutation kill rate as a percentage: mutants killed / mutants total × 100.
123///
124/// Mutation testing introduces small, artificial faults (flipping a
125/// comparison operator, changing a boundary condition) and checks whether
126/// the test suite fails against each mutated version. A high kill rate
127/// means the tests are genuinely verifying behaviour, not merely executing
128/// it; this is the check on test effectiveness itself, and coverage's
129/// necessary complement.
130///
131/// # Arguments
132///
133/// * `mutants_killed` — mutants the test suite detected (caused a failure).
134/// * `mutants_total` — total mutants generated and run against the suite.
135///
136/// # Returns
137///
138/// `Some(percentage)`, or `None` when `mutants_total` is zero (no mutants
139/// were generated — kill rate undefined).
140///
141/// # Examples
142///
143/// ```rust
144/// use software_engineering::test_effectiveness::mutation_kill_rate_percent;
145///
146/// // High coverage paired with a kill rate "under 40%": tests execute
147/// // code without meaningfully checking it.
148/// let kill_rate = mutation_kill_rate_percent(38.0, 100.0).unwrap();
149/// assert!(kill_rate < 40.0);
150/// assert_eq!(mutation_kill_rate_percent(1.0, 0.0), None);
151/// ```
152#[must_use]
153pub fn mutation_kill_rate_percent(mutants_killed: f64, mutants_total: f64) -> Option<f64> {
154    if mutants_total == 0.0 {
155        None
156    } else {
157        Some(mutants_killed / mutants_total * 100.0)
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    // "leadership had set a company-wide 95% coverage requirement for all
166    // new code, enforced as a hard CI gate."
167    #[test]
168    fn company_wide_coverage_requirement_is_95_percent() {
169        assert!((test_coverage_percent(95.0, 100.0).unwrap() - 95.0).abs() < 1e-9);
170    }
171
172    // "found a mutation-kill rate under 40% across much of the codebase."
173    #[test]
174    fn mutation_kill_rate_can_be_under_40_percent_despite_high_coverage() {
175        let kill_rate = mutation_kill_rate_percent(38.0, 100.0).unwrap();
176        assert!(kill_rate < 40.0);
177    }
178
179    // "mandatory mutation testing above an 80% kill-rate threshold for
180    // payment and authentication code."
181    #[test]
182    fn critical_code_policy_requires_at_least_80_percent_kill_rate() {
183        let kill_rate = mutation_kill_rate_percent(85.0, 100.0).unwrap();
184        assert!(kill_rate >= 80.0);
185    }
186
187    // Coverage measures execution over a total; a zero total (no code to
188    // cover) leaves the percentage undefined.
189    #[test]
190    fn coverage_is_none_when_total_is_zero() {
191        assert!(test_coverage_percent(1.0, 0.0).is_none());
192    }
193
194    // "a test suite with high line coverage but a low mutation-kill rate is
195    // executing code without meaningfully checking it" — a zero mutant
196    // count is equally undefined, not zero.
197    #[test]
198    fn mutation_kill_rate_is_none_when_no_mutants_were_run() {
199        assert!(mutation_kill_rate_percent(1.0, 0.0).is_none());
200    }
201}