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}