Definition
- Line coverage – Percentage of code lines executed by tests.
- Branch coverage – Percentage of decision branches (if/else) taken by tests.
- Path coverage – Percentage of execution paths through the code covered by tests.
- Useful signal – Highlights untested code that may hide bugs, especially in critical financial logic.
- Not a quality proof – Tests can execute code without meaningful assertions. 100% coverage does not mean 100% tested.
- Guide not target – Use coverage to find gaps, not as a vanity metric to chase.
At the National Bank of India, code coverage is like checking what fraction of the bank's procedures a drill actually practiced. Line coverage asks how many steps were walked through, branch coverage asks whether both the approve and reject paths of a loan decision were tried, and path coverage asks whether whole end-to-end routes were exercised. But a drill can touch every room without anyone checking that the right thing happened, so running the code is not the same as verifying it is correct. Chasing one hundred percent can even push the team to write pointless checks that touch trivial code just to raise the number, while the genuinely risky money logic deserves the real attention. The goal is to meaningfully test what matters, not to color the whole map green.
Understanding the Concepts
What is code coverage?
Code coverage measures how much of your source code is executed when tests run. It is a metric, not a quality proof.
# Generate coverage report with JaCoCo (Java)
mvn test jacoco:report
# Generate coverage report with Istanbul (JavaScript)
npx jest --coverage
What are the types of coverage?
Different types measure different aspects of code execution.
// Example code for coverage analysis
public BigDecimal calculateFee(Account account) {
if (account.getBalance().compareTo(BigDecimal.ZERO) > 0) { // Branch 1
if (account.isPremium()) { // Branch 2
return BigDecimal.ZERO; // Path A
} else {
return new BigDecimal("25.00"); // Path B
}
} else {
return new BigDecimal("50.00"); // Path C
}
}
| Type | What It Measures | Example |
|---|---|---|
| Line | Lines executed / total lines | 5 of 7 lines = 71% |
| Branch | Decision branches taken / total | 2 of 4 branches = 50% |
| Path | Execution paths covered / total | 2 of 3 paths = 67% |
| Function | Functions called / total functions | All functions called = 100% |
Why is 100% coverage not always the goal?
Coverage measures quantity, not quality. Tests can execute code without meaningful assertions.
// 100% line coverage but USELESS test
@Test
void coversEverything() {
AccountService service = new AccountService();
service.calculateFee(premiumAccount);
service.calculateFee(regularAccount);
service.calculateFee(zeroBalanceAccount);
// All lines executed: 100% coverage
// BUT: no assertions! We verified nothing.
}
// 50% coverage but VALUABLE test
@Test
void premiumAccountHasZeroFee() {
BigDecimal fee = service.calculateFee(premiumAccount);
assertEquals(BigDecimal.ZERO, fee);
// Covers one path but PROVES it works correctly
}
What does coverage tell you?
Coverage is useful as a gap-finder: untested code is a risk.
Coverage report shows:
- src/service/TransferService.java: 85%
- src/service/InterestCalculator.java: 95%
- src/service/FraudChecker.java: 30% <- gap!
The gap in FraudChecker is a risk signal.
It does NOT mean TransferService at 85% is well-tested.
It DOES mean FraudChecker likely has untested paths.
What are coverage thresholds?
Set minimum thresholds to catch coverage drops, but do not chase 100%.
// jest.config.js: coverage thresholds
{
"coverageThreshold": {
"global": {
"branches": 70,
"functions": 80,
"lines": 80,
"statements": 80
},
"./src/service/InterestCalculator.js": {
"branches": 95,
"lines": 95
}
}
}
<!-- JaCoCo Maven: fail build on low coverage --syn-kw">>
syn-kw"><rulesyn-kw">>
syn-kw"><elementsyn-kw">>BUNDLEsyn-kw"></elementsyn-kw">>
syn-kw"><limitssyn-kw">>
syn-kw"><limitsyn-kw">>
syn-kw"><countersyn-kw">>LINEsyn-kw"></countersyn-kw">>
syn-kw"><valuesyn-kw">>COVEREDRATIOsyn-kw"></valuesyn-kw">>
syn-kw"><minimumsyn-kw">>0.80syn-kw"></minimumsyn-kw">>
syn-kw"></limitsyn-kw">>
syn-kw"><limitsyn-kw">>
syn-kw"><countersyn-kw">>BRANCHsyn-kw"></countersyn-kw">>
syn-kw"><valuesyn-kw">>COVEREDRATIOsyn-kw"></valuesyn-kw">>
syn-kw"><minimumsyn-kw">>0.70syn-kw"></minimumsyn-kw">>
syn-kw"></limitsyn-kw">>
syn-kw"></limitssyn-kw">>
syn-kw"></rulesyn-kw">>
What is the right coverage strategy?
Focus coverage on the code that matters most, not on hitting a percentage.
High coverage priority:
- Financial calculations (interest, fees, balances)
- Security logic (auth, authorization, encryption)
- Business rules (loan approval, transfer limits)
Lower coverage priority:
- Simple getters and setters
- Configuration boilerplate
- Framework-generated code
Strategy: 90%+ on critical paths, 70%+ overall
What are common mistakes with coverage?
The first mistake is treating coverage as a quality metric. High coverage does not mean the code is well-tested; it means the code was executed.
The second mistake is chasing 100% by writing meaningless tests that execute code without asserting anything useful.
Wrong thinking:
"We have 95% coverage. Our code is well-tested."
Correct thinking:
"We have 95% line coverage. Let me check:
- Are the assertions meaningful?
- Is branch coverage also high?
- Are the critical money paths covered?
- What is in the untested 5%?"
What are common interview follow-up questions?
- "What coverage percentage do you target?" 80% overall with higher thresholds for critical business logic. The number matters less than what is covered.
- "Is branch coverage better than line coverage?" Yes. Line coverage can miss untested branches. Branch coverage is more thorough.
- "How do you improve coverage?" Start with the gap report, prioritize critical uncovered code, and write tests with meaningful assertions.
Key Takeaway
Code coverage measures how much of the production code the tests execute. Line coverage is the percentage of lines that ran, which is easy to read but weak because a line can execute without its logic being verified. Branch coverage, or decision coverage, is the percentage of branch outcomes taken, so for every if it checks that both the true and false sides were exercised, which is stronger because it forces tests past the happy path. Path coverage is the percentage of distinct end-to-end paths, the strictest but usually infeasible because paths grow combinatorially. Tools like JaCoCo compute these during a run and highlight the uncovered lines and branches.
Coverage is genuinely useful as a signal of what is not tested, and a sensible pipeline threshold stops it from regressing. But the crucial limitation is that coverage measures execution, not verification: a test can run a method's lines and assert nothing meaningful, yet those lines still count as covered, so a codebase can show ninety or even one hundred percent while its tests would not catch real defects. That is exactly why one hundred percent is not always the goal. Chasing a perfect number creates perverse incentives, engineers write tests for trivial getters and boilerplate just to raise the percentage, spending effort where the risk is near zero, while the genuinely complex money logic gets only superficial coverage, and the last few percent are usually the hardest and least valuable, defensive branches that are nearly impossible to trigger. So the mature approach is risk-based: aim for high, meaningful coverage of the important and risky logic, prefer branch coverage for decision-heavy code, write tests that assert correct behavior and cover the significant edge cases, and treat coverage as a diagnostic to find gaps rather than a target to maximize, pairing it with assertion quality and, where it matters, mutation testing to confirm the tests actually catch injected faults.
At the National Bank of India I once inherited a fee module reporting high line coverage, but branch coverage revealed that the premium-customer fee-waiver branch was never exercised, so a wrong fee could have hit premium customers undetected. I added an assertive test for that branch rather than padding the number with trivial getter tests. We set our JaCoCo gate at a sensible branch threshold on the critical packages rather than one hundred percent everywhere. Being able to define the metrics, explain that coverage proves execution not correctness, and argue for deep assertive coverage of the risky logic over a vanity number is what shows an interviewer I use coverage as an engineer rather than as a scoreboard.
Remember one sentence: code coverage is a useful signal of gaps but not a quality proof, because high coverage of the wrong things is less valuable than focused coverage of critical paths.
---
Visual Overview
Code Coverage Types
────────────────────
Line Coverage: How many lines were executed
Branch Coverage: How many decision branches were taken
Path Coverage: How many execution paths were covered
if (balance > 0) { Line: 2 lines
debit(amount); Branch: 2 (true, false)
} else { Path: 2 paths
reject();
}
100% line coverage != 100% branch coverage
100% branch coverage != meaningful assertions
Coverage is a gap-finder, not a quality proof