assertThat(new Pair(3, 2).maxComponent()).isEqualTo(3);Ivan Ponomarev
In the modern world, both code and tests are cheap
We need a way to measure the quality of our tests.
“What’s the test/code coverage in your project?”
“We have very high, 80%..90% test coverage”
“We need to improve coverage!”
“Can you give us 100% test coverage?”
“What’s the test/code coverage in your project?”
“We have very high, 80%..90% test coverage”
“We need to improve coverage!”
“Can you give us 100% test coverage?”

![]() | Ivan Ponomarev
|
assertThat(new Pair(3, 2).maxComponent()).isEqualTo(3); |
|
assertThat(new Pair(3, 2).maxComponent()).isEqualTo(3); |
|
Instruction coverage provides information about the number of instructions
that has been executed or missed.
![]() | ![]()
|
|
|
| ![]() |
| ![]() |
![]() | How many examples should we consider in order to reliably test this method? |
![]() | ![]()
|
![]() | ![]()
|
![]() | ![]()
|
![]() | ![]()
|
![]() | ![]()
|

Branch coverage (С1) doesn’t depend on the "length" of the branches — every decision point brings equal weight to the calculation of %.
This gives more pessimistic %, but this is actually healthier for the project (we’ll get to it).
My advice: switch from measuring line/instruction coverage to branch coverage now.
![]() |
All of them are 4 for this method, but it’s just a coincidence! |
![]() | For a chain of statements without any decision points, CC = 1 no matter how long is the chain!! E - N + 2 = 2 - 3 + 2 = 1 |
![]() | ![]() | ![]() | For a chain of statements without any decision points, CC = 1 no matter how long is the chain!! E - N + 2 = 2 - 3 + 2 = 3 - 4 + 2 = 4 - 5 + 2 = 1 (we add +1 edge and +1 node) |
![]() |
CC = E - N + 2 = 6 - 6 + 2 = 2 |
![]() |
CC = E - N + 2 = 11 - 10 + 2 = 3 |
![]() | Cyclomatic complexity
|
Introduced by Thomas J. McCabe in 1976.
It’s very easy to calculate for every programming language (only very basic lexical/syntax analysis is needed). On the other hand, estimation of actual possible paths of execution involves analysis of “possible” and “impossible” paths.
It’s additive (we can calculate CC for a whole project).
It’s “roughly the number of unit tests needed”.
MCabe’s categorization of CC of a single procedure (2008 PowerPoint deck→Wikipedia):
1 - 10: Simple procedure, little risk
11 - 20: More complex, moderate risk
21 - 50: Complex, high risk
> 50: Untestable code, very high risk
My own categorization (Ivan Ponomarev, 2026):
1 - 15 OK
> 15 mess, if it’s deterministically generated code — refactor
Cyclomatic complexity is a very easy to calculate metric, available for most languages.
Set up a gate for CC for no more than 15 per method in order to reduce bugs and surface problematic places in the code.
| ![]() |
![]() |
![]() | Wrong code:
instead of
|
![]() | ![]() ![]() |
![]() | ![]() ![]()
|
![]() | ![]() ![]()
|
![]() | ![]() ![]()
|
![]() | ![]() ![]()
|
Full branch coverage (C1) does not imply full execution paths coverage (C2) and thus may oversee bugs.
C2 is difficult to measure and ordinary tools do not measure C2.
Code coverage is a useful test-quality signal only at low levels.
Beyond the saturation point, much more path coverage
produces only tiny gains in line or branch coverage.
|
|
Setting an overly high code coverage threshold does not lead to real codebase improvement.
My suggestion: 75% is the highest reasonable quality gate for branch coverage.
Measure unit test and integration test coverage separately, and set separate quality gates for each. Instead of an inflated “combined coverage” number, you get a more accurate picture.
|
|
Takes tests and runs them against automatically modified (mutated) versions of code
Mutated versions should fail the tests! If test indeed fails, it means that the mutation is killed (which is good), otherwise it means that the mutation is survived (which means that the test has a flaw!)
|
If mutations are generated randomly, it’s possible that mutated code will be semantically equivalent:
a + b → b + a
if (x > y) return x else return y → if (x >= y) return x else return y

long tax;
long allowance = 10000; // 10000 → 10001
if (income > 25000) { // > → >=, 25000 → 25001
long reduction = (income - 25000) / 2; // 25000 → 25001
allowance = Math.max(0, allowance - reduction); // 25000 → 1
}
long taxableIncome = Math.max(0, income - allowance); //max → argument, 0 → 1
if (taxableIncome <= 100000) { // < → <=, 100000 → 100001
tax = Math.round(taxableIncome * .25);
} else {
tax = Math.round(100000 * .25);
long remaining = taxableIncome - 100000; //100000 → 100001
tax = tax + Math.round(remaining * .35);
}
return tax;Test case for income below allowance worth adding, as well as tests for boundary values.
> → >= mutations are equivalent mutations, as tax value is a continuous function, and these are false positives.
SLOW with real-life code (2 mins → 40 mins).
Equivalent mutations give false-positive "deficiencies".
Modern tools fight both of these problems, the fight goes on
(see, for example, "Jan-Jelle Kester. Stryker: How mutation testing got practical", FOSDEM 2024)
For mission-critical pieces of code, mutation testing provides high level of confidence, as it measures the quality of tests themselves.
Mutation testing results should be taken with caution: false positives occur among genuine defects.
Mutation testing is slow.
In real life, mutation testing is still considered to be impractical and is used quite rarely.
In the modern world, both code and tests are cheap,
and code coverage metric is inflated and devalued.
Make it meaningful again: calculate it precisely,
and reject high but meaningless numbers.
Stop using the C0 “covered lines” metric. Switch to C1 branch coverage today. The numbers will be lower, but more honest.
Set a maximum cyclomatic complexity of 15 per method across your codebase.
Avoid vanity coverage thresholds of 80% or higher. A practical target is 60–75%.
Measure unit test and integration test coverage separately, and set separate quality gates for each.
Look at the coverage report, not just the metric. Find potentially risky fragments and apply techniques such as fuzzing and/or mutation testing to critical code.
Keep an eye on modern tooling. Practical tools for C2 and mutation coverage are improving, but there is still more to come.
Charles Goodhart’s Law: "Every measure which becomes a target becomes a bad measure"
Slides-as-code: https://github.com/inponomarev/coverage-talk
@inponomarev