1. Home
  2. Blog
  3. Software Testing

Software Testing

Code Coverage Explained: Line vs Branch Coverage and What 100% Really Means

Coverage tells you which code your tests ran. It does not tell you whether the tests checked anything. We measured a small module three ways to show the difference.

8 min read
Code coverage explained: line vs branch coverage and what 100% really means

Key takeaways

  • Code coverage measures how much of your code runs during tests.
  • Branch coverage is stricter than line coverage, and more useful.
  • In our test, a suite with no assertions still reached 100% coverage.
  • Use coverage to find untested code, not as proof of quality.

A team proudly reports 95% code coverage, then ships a bug in a line that was “covered”. This is not a contradiction. Coverage answers one narrow question: did the tests execute this code? Whether they checked the result is a different question entirely.

We used Python with coverage.py 7.17 and pytest 9.2. Every number and report below is from our own run. The ideas apply in any language.

What is code coverage?

Code coverage is the percentage of your code that runs while your automated tests run. A tool records which lines, and optionally which branches, were executed, then reports what was missed.

It is most useful in reverse. Low coverage in an area tells you, with certainty, that tests are not exercising it. High coverage tells you much less.

Types of coverage

Type Question it answers
Function Was each function called at least once?
Line (statement) Did each line run at least once?
Branch Did each if go both ways, true and false?
Condition Did each part of a combined condition (a and b) take both values?
MC/DC Did each condition independently change the outcome? Used in safety-critical software.

Each level is stricter than the one above it. Most teams track line and branch coverage. Condition coverage and MC/DC appear in aviation and automotive standards, where tooling support is specialised.

The code we measured

def shipping_fee(total, is_member, express=False):
    if total < 0:
        raise ValueError("total cannot be negative")
    fee = 0 if total >= 1000 else 50
    if is_member:
        fee = max(fee - 25, 0)
    if express:
        fee += 100
    return fee

Our first test file had two tests:

def test_regular_order():
    assert shipping_fee(500, False) == 50

def test_member_express():
    assert shipping_fee(500, True, express=True) == 125

We ran it with branch measurement switched on:

coverage run --branch -m pytest test_shipping.py
coverage report -m

Reading the report

Name          Stmts   Miss Branch BrPart  Cover   Missing
---------------------------------------------------------
shipping.py       9      1      6      1    87%   3
  • Stmts 9, Miss 1: nine executable statements, one never ran.
  • Branch 6, BrPart 1: three if decisions give six possible outcomes. One decision only ever went one way.
  • Missing 3: line 3, the raise ValueError, never ran. No test passed a negative total.

The fix is a test for the error path. We also added a test for a large order:

def test_free_over_1000():
    assert shipping_fee(1000, False) == 0

def test_negative_total():
    with pytest.raises(ValueError):
        shipping_fee(-1, False)

With both files, the report showed 100% and no missing lines or partial branches. Error paths are the classic gap coverage finds, which is why tests for invalid input, designed with techniques like equivalence partitioning, pair so well with it.

What the report hid

Look at the first report again. It said only line 3 was missing. But the first two tests both used a total of 500, so the free shipping rule for orders of 1000 or more was never tested, and the report did not flag it.

The reason is that fee = 0 if total >= 1000 else 50 sits on one line. coverage.py measures branches between lines, so a conditional expression inside a single line counts as one executed statement. If that rule had a bug, such as > instead of >=, the first suite would still have shown 87% with no hint. Tests of the boundary itself, as described in boundary value analysis, are what catch it.

100% coverage with no checks

Next, we wrote a deliberately poor test that calls every path but asserts nothing:

def test_runs_everything_but_checks_nothing():
    shipping_fee(500, True, express=True)
    shipping_fee(1000, False)
    shipping_fee(500, False)
    try:
        shipping_fee(-1, False)
    except ValueError:
        pass
shipping.py       9      0      6      0   100%

It reached 100% line and branch coverage. The function could return any number at all and this test would still pass. This is the core limit of coverage: it measures execution, not verification.

Tip: When reviewing a pull request, read the assertions, not just the coverage change. A test that adds coverage without a meaningful assert only adds false confidence.

To measure whether tests actually catch bugs, use mutation testing. A tool such as mutmut for Python, PIT for Java or Stryker for JavaScript makes small changes to your code, such as flipping >= to >, and checks that a test fails. Our no-assert test would let every mutation survive.

What target to set

There is no universally correct number. Practical guidance:

  • Do not chase 100%. The last few percent often means testing trivial code or writing tests that just touch lines.
  • Many teams aim for roughly 70 to 85% overall, with higher expectations for core business logic.
  • Watch the trend, not the number. Coverage that drops on each release is a warning sign.
  • Require coverage of new code in pull requests rather than forcing old code up all at once.
  • Exclude what does not matter, such as generated code, with clear rules.

Goodhart’s law applies: when a measure becomes a target, it stops being a good measure. Hard targets encourage tests like our no-assert example.

Tools by language

Language Common tools
Python coverage.py, pytest-cov
JavaScript / TypeScript Istanbul (nyc), built into Jest and Vitest
Java JaCoCo
.NET Coverlet
Go go test -cover
Reporting Codecov, SonarQube, Coveralls

Coverage in CI

Run coverage as part of your CI pipeline, and publish the report so reviewers can see which new lines are untested. With coverage.py, coverage report --fail-under=80 fails the build below a threshold. Keep any threshold modest and combine it with a rule on new code, so a single large legacy file does not block every change.

Beyond coverage: better questions to ask

  • Do tests cover the risky, frequently changed parts of the code?
  • Are boundaries and error paths tested, not just the happy path?
  • Would a test fail if this line were wrong?
  • Are the tests fast and stable enough to be run on every change?

These belong in any test strategy. Coverage helps answer only the first.

Excluding code and reading HTML reports

Some code is not worth testing directly, such as debug helpers or a block that only runs when a script is started by hand. coverage.py lets you exclude lines with a comment:

if __name__ == "__main__":  # pragma: no cover
    main()

You can also set exclusions and the source folders to measure in configuration, for example in pyproject.toml:

[tool.coverage.run]
branch = true
source = ["shop"]

[tool.coverage.report]
show_missing = true
exclude_also = ["if TYPE_CHECKING:"]

Use exclusions sparingly and review them like code. Each one is a decision that some lines will never be checked. Excluding a hard-to-test error handler just to lift the number defeats the purpose.

For day-to-day work, the HTML report is more useful than the terminal table. Run coverage html and open htmlcov/index.html. Each file is shown with executed lines in green, missed lines in red and partial branches in yellow, so you can see at a glance which if never took its other path. Many editors can also show coverage in the margin, which makes gaps visible while you are writing the code rather than afterwards.

Coverage before refactoring

Coverage is also a safety check before refactoring. If you plan to restructure a module, look at its report first. Red areas have no tests to warn you if behaviour changes, so add characterisation tests that capture the current behaviour before you start. Used this way, coverage stops being a target and becomes a practical planning tool.

The same report helps when a bug escapes to production. Find the line where the bug lived and check whether any test ran it. If not, you have found a gap worth closing. If a test did run it, the problem is a weak assertion, which is exactly the situation our no-assert example showed. Both answers tell you how to improve the suite.

Final thoughts

Code coverage is a flashlight, not a grade. Use branch coverage to find code that no test touches, especially error handling, and then write tests that make real assertions. If you remember one result from our run, make it this one: 100% coverage was reached by a test that checked nothing.

Frequently asked questions

What is code coverage in testing?

It is the percentage of code executed while automated tests run, measured by lines, branches or conditions. It shows what tests touched, not whether they verified it.

What is the difference between line and branch coverage?

Line coverage checks that each line ran. Branch coverage checks that each decision went both ways. Branch coverage finds untested paths that line coverage misses.

Is 100% code coverage good?

Not by itself. In our test, a suite with no assertions reached 100%. Aim for meaningful tests on important code rather than a perfect number.

What is a good code coverage percentage?

Many teams target roughly 70 to 85% overall, with more on critical logic. The trend and the quality of the assertions matter more than the exact number.

How is mutation testing different from coverage?

Mutation testing changes your code slightly and checks that a test fails. It measures whether tests detect bugs, which coverage cannot do.

Keep learning

Work with Stack Quora

Need help with testing, WordPress or SEO?

We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.

Keep reading

More guides on testing, AI & modern development

Enjoyed this article? Pick a topic and jump straight into our newest tutorials.

Artificial Intelligence

LLM Structured Output Explained: Getting Reliable JSON From AI Models

LLM structured output explained with validation we ran in Pydantic: JSON mode and schemas, constrained decoding, the ways model output…

Read article →
DevOps

SSH Keys Explained: How They Work, ssh-keygen, Ed25519 and Safe Habits

SSH keys explained with keys we generated: public vs private keys, ssh-keygen, Ed25519 vs RSA, fingerprints, passphrases, ssh-agent, authorized_keys, config…

Read article →
DevOps

HTTP Caching Explained: Cache-Control, ETag and 304 Responses, With a Live Test

HTTP caching explained with a small server we ran: Cache-Control directives, max-age, no-cache vs no-store, ETag revalidation and 304 responses,…

Read article →
Test Automation

API Contract Testing Explained: Catch Breaking Changes Before They Ship

API contract testing explained with a schema check we ran: what a contract is, consumer-driven contracts, Pact, OpenAPI, which changes…

Read article →
Software Testing

Risk-Based Testing Explained: Score Risks, Build a Matrix and Test What Matters

Risk-based testing explained step by step: identifying risks, scoring likelihood and impact, building a risk matrix, mapping risk to test…

Read article →
Databases & SQL

SQL Constraints Explained: NOT NULL, UNIQUE, CHECK and FOREIGN KEY, Tested

SQL constraints explained with tests we ran: NOT NULL, UNIQUE, CHECK, DEFAULT, PRIMARY and FOREIGN KEY, the NULL loopholes, SQLite's…

Read article →
Databases & SQL

SQL Date Functions Explained: Adding, Comparing and Grouping Dates Without Bugs

SQL date functions explained with queries we ran: adding intervals, month-end surprises, date differences, the BETWEEN trap, grouping by day,…

Read article →
Software Development

Python asyncio Explained: async, await, gather and TaskGroup With Real Timings

Python asyncio explained with code we timed: coroutines, await, gather, TaskGroup, error handling, timeouts, limiting concurrency and the blocking-call mistake…

Read article →
Software Development

Debounce vs Throttle in JavaScript: How They Work, When to Use Each, Tested

Debounce vs throttle in JavaScript explained with timings we measured in Node.js: how each works, simple implementations, leading and trailing…

Read article →
Software Development

Python Context Managers Explained: The with Statement, contextlib and Real Uses

Python context managers explained with code we ran: how with works, __enter__ and __exit__, contextlib.contextmanager, suppress, ExitStack, database transactions and…

Read article →
Artificial Intelligence

LLM Temperature and Top-p Explained: How AI Models Choose Their Next Word

LLM temperature and top-p explained with a simulation we ran: how probabilities are reshaped, what top-p keeps, recommended settings by…

Read article →
DevOps

YAML Explained: Syntax, the Norway Problem and Other Traps We Tested

YAML explained with a parser test: basic syntax, lists and maps, the Norway problem, versions turning into floats, octal zip…

Read article →
DevOps

Cron Jobs Explained: Syntax, Real Schedules and the Mistakes That Break Them

Cron jobs explained with schedules we verified: the five fields, operators, real examples, the day-of-month OR day-of-week trap, environment problems,…

Read article →
Software Testing

Bug Life Cycle in Software Testing: Every Status, Who Owns It and Common Mistakes

The bug life cycle explained: every defect status from New to Closed, who owns each step, rejected, deferred and reopened…

Read article →
Test Automation

Property-Based Testing in Python: Hypothesis Finds the Bugs Your Examples Miss

Property-based testing explained with Hypothesis tests we ran: properties vs examples, strategies, shrinking, the real bugs it found in our…

Read article →
Databases & SQL

SQL Pagination Explained: OFFSET vs Keyset, With a Million-Row Test

SQL pagination explained with a million-row test we ran: LIMIT and OFFSET, why deep pages get slow, keyset (cursor) pagination,…

Read article →
Databases & SQL

SQL EXISTS vs IN: Differences, the NOT IN NULL Trap and Performance

SQL EXISTS vs IN explained with queries we ran: how each works, why NOT IN returns no rows when a…

Read article →
Software Development

Python Logging Explained: Levels, Handlers, Formatters and Mistakes to Avoid

Python logging explained with code we ran: levels, basicConfig quirks, named loggers, handlers and formatters, logger.exception, lazy formatting, duplicate logs…

Read article →
Software Development

JavaScript Map vs Object: Key Differences, Pitfalls and a Benchmark

JavaScript Map vs Object explained with code we ran in Node.js: key types, ordering, the prototype trap, JSON, size, WeakMap…

Read article →
Software Development

Python Type Hints Explained: Annotations, mypy and the Errors It Catches

Python type hints explained with a real mypy run: basic annotations, Optional and unions, TypedDict, Literal, Protocol, generics, Callable, strict…

Read article →
Artificial Intelligence

LLM Tokens Explained: How AI Models Read Text, Count Costs and Fill Context Windows

LLM tokens explained with a tokenizer we ran: how text is split, why counts differ by language and tokenizer, context…

Read article →
DevOps

Semantic Versioning Explained: MAJOR.MINOR.PATCH, Ranges and Release Rules

Semantic versioning explained with code we ran: MAJOR.MINOR.PATCH rules, pre-releases, correct version sorting, caret and tilde ranges, 0.x versions, lockfiles…

Read article →
DevOps

Git Reset vs Revert: How to Undo Commits Safely, With a Tested Example

Git reset vs revert explained with a repository we built: soft, mixed and hard reset, revert commits, reverting merges, recovering…

Read article →
Software Testing

The Test Pyramid Explained: Layers, Ratios and When to Break the Shape

The test pyramid explained: unit, integration and end-to-end layers, sensible ratios, the ice cream cone anti-pattern, the testing trophy and…

Read article →
Test Automation

pytest Fixtures Explained: Scope, yield Teardown, conftest.py and Factories

pytest fixtures explained with a suite we ran: how fixtures work, scopes, yield teardown, conftest.py, factory fixtures, parametrized fixtures, built-ins…

Read article →
Databases & SQL

SQL Upsert Explained: INSERT ON CONFLICT, MERGE and the REPLACE Trap

SQL upsert explained with queries we ran: INSERT ON CONFLICT DO UPDATE and DO NOTHING, the excluded row, conditional updates,…

Read article →
Databases & SQL

SQL LIKE Operator Explained: Wildcards, Case Sensitivity and Performance

The SQL LIKE operator explained with queries we ran: % and _ wildcards, escaping, NOT LIKE and NULL, case sensitivity…

Read article →
Artificial Intelligence

AI Hallucinations Explained: Why Language Models Make Things Up and How to Reduce It

AI hallucinations explained: what they are, why language models produce them, common types, how to reduce them with grounding and…

Read article →
Test Automation

Mocking in Python: unittest.mock, patch and the Mistakes That Make Tests Lie

Mocking in Python with unittest.mock, tested: Mock and return_value, side_effect, patching where a name is used, spec and autospec, mocking…

Read article →
Software Testing

Code Coverage Explained: Line vs Branch Coverage and What 100% Really Means

Code coverage explained with a real coverage.py run: line, branch and condition coverage, reading a report, why 100% coverage can…

Read article →
Artificial Intelligence

What Are AI Agents? How They Work, With a Simple Agent Loop You Can Run

What are AI agents? A plain guide to the agent loop, tools, memory and guardrails, with a small Python loop…

Read article →
Test Automation

TDD vs BDD: Key Differences, One Feature Built Both Ways and When to Use Each

TDD vs BDD explained with one discount feature built both ways: red-green-refactor with code we ran, Given-When-Then scenarios, tools, trade-offs…

Read article →
Software Testing

User Acceptance Testing (UAT): Process, Checklist and Real Examples

User acceptance testing explained: who runs it, how it differs from QA, a seven-step process, a UAT test case template,…

Read article →
Artificial Intelligence

What Are Embeddings? Vectors and Cosine Similarity Explained Simply

What are embeddings? A plain-language guide to vectors, cosine similarity and semantic search, with a small example we ran, common…

Read article →
Test Automation

Flaky Tests: What Causes Them and How to Fix Them for Good

Flaky tests explained with demos we ran: the main causes, how to confirm and diagnose one, proper fixes for timing,…

Read article →
Software Testing

Load Testing vs Stress Testing: Differences, Metrics and k6 Examples

Load testing vs stress testing explained: what each proves, spike and soak tests, the metrics that matter, why averages mislead,…

Read article →