What is code coverage?
Code coverage (sometimes called test code coverage) answers one question: when the test suite runs, which parts of the code are executed, and which are never touched? A coverage tool records this while the tests run and produces a report — a percentage for the whole project, per file, and often a line-by-line view that highlights untested code.
Its value is in what it reveals about the gaps. Code that no test executes can't be protected by tests: if it breaks, nothing fails. A coverage report is the fastest way to find those blind spots — an error-handling branch never exercised, a feature flag path nobody tested, a module added without tests.
How code coverage is calculated
Coverage tools instrument the code — either by rewriting it before the tests run (as Istanbul does for JavaScript) or by using hooks in the runtime (as coverage.py and V8 coverage do). Each line, branch or function gets a counter. After the run, the tool compares what executed with what exists.
The formula is the same for every metric: covered items divided by total items, multiplied by 100. If a file has 200 executable lines and the tests execute 150 of them, line coverage is 150 ÷ 200 × 100 = 75%. If the same file has 40 decision outcomes (each if has two: true and false) and the tests take 24 of them, branch coverage is 60% — lower, because tests often skip the error paths.
Code coverage metrics: line, branch, function and more
Different metrics count different things. Line coverage is the number most tools show first, but branch coverage is the more honest one, because it catches untested else-branches and error paths that line coverage reports as covered.
| Metric | What it counts | Catches |
|---|---|---|
| Function coverage | Functions called at least once | Entirely untested functions |
| Line / statement coverage | Executable lines or statements run | Untested blocks of code |
| Branch coverage | Both outcomes of every decision (if/else, switch cases, ternaries) | Untested error paths and else-branches |
| Condition coverage | Every boolean sub-expression evaluated true and false | Conditions that never change the result in tests |
| MC/DC | Each condition shown to independently affect the outcome | Required for the most critical avionics software (DO-178C Level A) |
What is test coverage?
Test coverage asks a different question: of the things this software must do, how many have a test that would fail if they broke? It is measured against a list of requirements, user flows, business rules or risks — not against lines of code — so it can't be produced automatically by a tool, but it is much closer to what matters to users.
Common test coverage techniques include:
- Requirements coverage. Map every requirement or acceptance criterion to at least one test (a traceability matrix).
- Risk-based coverage. Test most heavily where failure would cost most: payments, permissions, data deletion, migrations.
- Boundary value analysis. Test at and around the edges of valid input — 0, 1, the maximum, one past the maximum.
- Equivalence partitioning. Group inputs that should behave the same and test one value from each group.
- User-journey coverage. End-to-end tests for the critical paths users take through the product.
Code coverage vs test coverage
The two are complementary. Code coverage tells you which code no test touches; test coverage tells you whether the behaviour that matters is actually checked. A project can have 95% code coverage and still miss a requirement entirely, or have every requirement tested while large areas of code are never executed.
| Code coverage | Test coverage | |
|---|---|---|
| Measures | Code executed by tests | Requirements and risks checked by tests |
| Unit | Lines, branches, functions | Requirements, user flows, risks |
| How it's produced | Automatically, by a coverage tool | Manually, by mapping tests to requirements |
| Answers | Which code is never tested? | Is the behaviour that matters verified? |
| Main weakness | Execution isn't verification | Depends on the requirement list being complete |
Why 100% code coverage can still miss bugs
Coverage records execution, not assertions. The first test below runs every line and both branches of the function — 100% line and branch coverage — yet it would pass even if the discount were wrong, because it checks nothing. The second test has the same coverage and actually verifies the behaviour.
function finalPrice(price: number, isMember: boolean) {
return isMember ? price * 0.9 : price;
}
test("finalPrice runs", () => {
finalPrice(100, true);
finalPrice(100, false); // 100% lines and branches — nothing asserted
});
test("members get 10% off", () => {
expect(finalPrice(100, true)).toBe(90);
expect(finalPrice(100, false)).toBe(100);
});What is a good code coverage percentage?
There is no universal number. Google's published guidance on code coverage describes roughly 60% as acceptable, 75% as commendable and 90% as exemplary — and warns against treating any threshold as the goal in itself. Many teams settle around 70–80% overall, with higher expectations for critical code such as billing, permissions and data handling.
Chasing the last few percent is rarely worth it: the remaining code is often trivial getters, generated code or hard-to-reach failure handling, and tests written only to raise the number tend to assert little. A falling coverage trend is usually more informative than the absolute value.
Code coverage tools by language
Every mainstream language has a mature coverage tool, and most CI services and code-quality platforms can read their reports.
| Language | Coverage tool | Notes |
|---|---|---|
| JavaScript / TypeScript | Istanbul (nyc), V8 coverage (c8) | Built into Jest and Vitest |
| Python | coverage.py | Usually run through pytest-cov |
| Java / Kotlin | JaCoCo | Maven and Gradle plugins |
| Go | go test -cover | Built into the Go toolchain |
| C / C++ | gcov, llvm-cov | Compiler-based instrumentation |
| .NET (C#) | Coverlet | Works with dotnet test |
| Ruby | SimpleCov | Common with RSpec and Minitest |
| PHP | PHPUnit with Xdebug or PCOV | PCOV is faster for coverage only |
| Swift | Xcode code coverage | Enabled per test scheme |
Measuring code coverage in CI
Run coverage on every pull request and fail the build when it drops below a threshold. Services such as Codecov and Coveralls, and platforms such as SonarQube, import the reports to show coverage trends and the coverage of each pull request's changes — but they don't measure coverage themselves; your test runner does.
# Jest (jest.config.js)
coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } }
# Python
pytest --cov=myapp --cov-fail-under=80
# Go
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.outBetter than one global number: diff coverage and mutation testing
Diff coverage (also called patch coverage) requires that new and changed lines in a pull request are covered. It stops coverage from eroding without forcing anyone to retrofit tests onto old code, and it puts the conversation where it belongs — on the change being reviewed.
Mutation testing measures whether tests would actually catch a bug. Tools such as Stryker (JavaScript, C#), PIT (Java) and mutmut (Python) make small changes to the code — flip a condition, change a constant — and check that at least one test fails. A surviving mutant points at code that is executed but not verified, exactly the gap line coverage hides.
Reviewing tests in pull requests
Coverage reports tell reviewers where tests are missing; they don't say whether the tests are good. In code review, check that tests assert on outcomes, that the failure paths the change introduces are tested, and that the tests would fail if the behaviour described in the ticket broke. Watch for tests that were deleted or skipped to make the build pass, and for mocks that replace the very behaviour under test.
Code coverage review checklist
- Every new or changed behaviour has a test that would fail if it broke.
- Tests assert on results, not just that code runs without throwing.
- Error and edge-case branches added by the change are tested.
- Diff coverage on the pull request meets the team threshold.
- Branch coverage, not only line coverage, is tracked.
- Critical paths — payments, permissions, data deletion — have stronger coverage.
- No tests were deleted or skipped to make the build pass.
- Mocks don't hide the behaviour the test is meant to check.
How Mesrai helps with test coverage
Mesrai reviews the tests in a pull request alongside the code: it points out changed behaviour with no test, tests that assert nothing, and error paths left untested. Teams can make expectations explicit with Mesrai Rules — for example, requiring tests for every change under a payments directory.
- Flags untested behaviour and assertion-free tests
- Coverage expectations as Mesrai Rules, scoped by path
- Business logic validation against the linked ticket
- Comments on the pull request where the gap is