Coverage measures executed lines, not asserted behavior; it is a lower bound on risk
Coverage counts which lines, branches, or statements were executed during a test run. Line coverage tells you a line ran; branch coverage tells you both sides of a conditional were taken. Neither tells you whether the test asserted anything meaningful about the result. A test that calls a function and asserts nothing can achieve 100% line coverage and still miss every bug. Coverage is best used as a diagnostic for finding untested areas and as a ratchet to prevent regressions in coverage percentage, not as a target. High coverage with weak assertions gives false confidence, while lower coverage concentrated on critical paths with strong assertions is often better. Also be aware that coverage tools trace execution, which adds overhead and can miss behavior triggered only by threading, subprocesses, or error paths that never run in the test suite.
Line coverage: which statements executed. Branch coverage: which outcomes of conditionals were taken. Path coverage is rarely practical.
Mutation testing, such as mutmut or cosmic-ray, is a stronger signal because it checks whether tests fail when code is subtly changed.
Assertion-free tests inflate coverage. Review tests for meaningful assertions, not just execution.
Coverage cannot detect missing requirements. A feature can be fully uncovered by tests and still show 100% coverage of the code that exists.
Trade-off: chasing 100% coverage slows delivery and encourages gaming, while ignoring coverage leaves blind spots.
Common mistake: enforcing a global percentage threshold across all modules regardless of criticality. Thresholds should be higher on core logic and lower on glue code.
Version note: coverage.py supports branch coverage and subprocess coverage via COVERAGE_PROCESS_START. Combining with pytest-cov is standard practice.
0-2 years experience
2-5 years experience
5-8 years experience
8+ years experience