Verdict
Yes
Yes, 80% code coverage is enough as a number for a product codebase; stop writing tests to raise it, give each feature that no test runs one test of its main path, and fully test uncovered code that moves money or checks access.
Why
- Blast radiususers
- Change frequencyregularly
- Detectabilitysame-day
- Reversibilitytrivial
- Test costmoderate
Yes, 80% coverage is enough as a number; spend the next hours on what the uncovered 20% contains. The typical case is a product team at 80% line coverage whose uncovered code holds error branches, startup wiring and untested features. Blast radius is users, and Change frequency is regularly, because those features change monthly. Detectability is same-day, because users report a broken page that day, and Reversibility is trivial, since a redeploy fixes it. Test cost is moderate, since each test needs a stub or a database, so rule R12 gives Test minimally: one main-path test for each untested feature.
| When | Decision | Why |
|---|---|---|
| An uncovered branch calculates a refund or a discount | Test mandatory: every rule and each rounding boundary | Blast radius rises to money and Reversibility to costly |
| An uncovered branch decides whether a user may read another account's records | Test mandatory: one allowed and one denied request for each role | Blast radius rises to safety-or-legal and Detectability to never, because a leaked record raises no error |
| A contract or a standard such as DO-178C for avionics requires a coverage figure | Test mandatory: meet the figure and keep each report as evidence | Blast radius rises to safety-or-legal, and the framework puts required evidence first |
| An uncovered catch block saves a profile without its address when a geocoding call fails | Test: force the failure with a stub and assert the saved row | Detectability moves to eventually and Reversibility to with-effort, because the row looks normal and needs a repair script |
| The application changes a few times a year | Do not test the uncovered code; check each change by hand before release | Change frequency falls to rarely |
| The uncovered lines register dependencies with one implementation per interface, and CI starts the application with a container that resolves every service at startup | Do not test the wiring; a missing registration stops the CI build | Detectability falls to immediately, and Reversibility stays trivial |
| An uncovered branch reindexes search after a timeout, and forcing the timeout takes days of setup | Test it differently: alert when the search index falls behind the database | Test cost rises to heavy and Detectability to eventually, while Reversibility is with-effort |
What breaks if you don't test
A codebase at 80% can have its CSV export at 0%, so a change to a shared date helper breaks the export with CI green, and a customer reports the empty file next day. The same total can hide a refund branch or an access check at 0%, which fail without an error.
What you lose if you over-test
The next ten points are the hardest lines: stubs that throw on the third call and tests for logging and startup code, which break when a refactor moves a line. A gate counts lines that ran, not results a test checked, so developers under a 90% target write tests that assert nothing. Google's testing team calls 75% "commendable" and writes that gains beyond a certain point are logarithmic (Google Testing Blog, 2020).
How to test
- Read the per-file report, not the total: pytest-cov with
--cov-report=term-missing, or Jest's--coverage, lists uncovered lines per file. - Give each feature file near 0% one test of its main path, at the cheapest level.
- Run the five factors on uncovered blocks that move money, check access or save data, and write the tests the conditions table names.
- Keep a gate at today's 80% as a floor, not a goal, and require all branches in risky folders with Jest's coverageThreshold:
// jest.config.js
module.exports = {
coverageThreshold: {
global: { lines: 80 }, // today's level, kept as a floor
"./src/billing/": { branches: 100 },
"./src/permissions/": { branches: 100 },
},
};
When the answer changes
- The uncovered 20% includes a billing, permissions or data import folder.
- A standard or a contract names a coverage figure.
Cost estimate + Code example
What the next ten points cost
My estimate for going from 80% to 90%, assumptions labelled:
- 50,000 lines of application code, my assumption for a mid-sized backend: 5,000 more lines to cover.
- One new test per 20 uncovered lines, also my assumption: 250 tests.
- One hour per test, the framework's figure for moderate Test cost: 250 hours, about six developer-weeks.
- One user-reported bug a month from those lines, my assumption, at four hours each to fix and redeploy: 48 hours a year.
- 30 uncovered blocks that move money or check access, my assumption, at three one-hour tests each: 90 hours.
The 250 hours pay back 48 hours a year of fixes in about five years, before upkeep. A wrong refund or a leaked record costs more than four hours, and the 30 money and access blocks take 90 hours.
Related questions
FAQ
- Is 80 percent test coverage good?
Yes, 80% line coverage is good for a product codebase when the uncovered 20% has no untested feature, money calculation or access check. Give each file near 0% one main-path test.
- Should I raise code coverage from 80% to 90%?
No, do not write tests only to raise coverage to 90%, because the next ten points are the hardest to reach and their failures show the same day. Test uncovered blocks that move money, check access or save data instead.
- Should CI fail below 80% code coverage?
Yes, fail CI below 80% when 80% is today's level, as a floor, not a goal. A gate above today's level makes developers write tests that assert nothing (Should I aim for 100% test coverage?), so put stricter thresholds on billing and permissions folders.
- Does 80% coverage mean 80% of the code is tested?
No, 80% coverage means 80% of lines ran during the tests, not that any test checked their results. Mutation testing shows which covered lines could break without a test failing.