The test coverage requirement decision record: why the coverage threshold model you chose determines your regression detection confidence ceiling and your test investment return failure mode
The test coverage requirement decision is not a decision about a number. It is a set of founding decisions about which metric the number measures, whether the threshold applies to aggregate coverage or to the distribution across critical paths, and whether a gate that blocks merges is the enforcement model or a trend dashboard is. These decisions together determine two structural properties that are invisible in sprint velocity dashboards and release frequency metrics but visible the moment a regression in a payment flow escapes to production with an 85% coverage badge on the repo: the regression detection confidence ceiling — the degree to which your coverage percentage predicts whether the code paths that matter are actually exercised by the test suite — and the test investment return failure mode — the systematic incentive that aggregate coverage metrics create for engineers to write tests that maximize the number rather than tests that catch regressions in code paths that matter. Three failure patterns: a company with 80% aggregate line coverage ships a payment regression because the 80% is concentrated in model validation and utility code while the payment processing path has 12% coverage; a company with 100% frontend line coverage ships a broken multi-step user flow because individual component rendering tests produce the 100% while state management and data flow between components is untested; and a company that suspends the coverage gate during a growth sprint finds coverage at 31% six months later because no one reinstated it and every team had quietly been operating without it.
A 34-person SaaS company built a professional services project management platform. Their engineering team had grown from 4 to 9 engineers over 18 months. Midway through that growth, the VP Engineering had introduced a code coverage requirement: all PRs had to maintain at least 80% line coverage across the codebase, enforced via a Jest coverage gate in CI. The decision was made carefully at the time — the VP had come from a larger company with strong testing culture and believed that 80% was a meaningful floor that would prevent the kind of untested code accumulation that had caused problems on prior teams. The reasoning was sound and the decision was implemented correctly. Engineers knew about it. CI enforced it. The coverage badge on the repository read 84%.
In month 22, the company shipped a new pricing tier that included a volume discount applied at checkout. The feature was built by a senior engineer who had joined the team four months prior. The checkout and payment processing code was a complex module that had existed since the company's founding — it had been written by the founding CTO, who had left the company six months before this engineer joined. The module had accumulated 14 months of payment logic: discount application, proration for mid-cycle upgrades, Stripe webhook handling for failed payments, and a dunning flow for recovering failed subscriptions. The founding CTO had not written tests for most of it — his philosophy was that payment code should be tested at the Stripe test environment level, not with unit tests, and he had relied on manual QA before each release. When he left, the team inherited the module with 12% line coverage: 27 lines of utility functions for currency formatting and date arithmetic were covered by utility tests; the 196 lines of actual payment orchestration were not.
The aggregate coverage was 84% because the rest of the codebase — models, serializers, utility functions, API route handlers for non-payment features — was well-covered. The payment module's 12% contribution to the aggregate was diluted by the volume of tested code elsewhere. No alert fired. No CI step failed. The coverage badge stayed green at 84%. The senior engineer building the new pricing tier added tests for the new discount calculation logic — her feature was well-tested, and she passed the coverage gate. She did not add tests to the existing payment orchestration code because that was not her feature and the module was complex and the founding CTO had not documented it. The discount application logic interacted with the proration calculation in a way that produced an incorrect charge amount for customers upgrading from a monthly plan to an annual plan mid-cycle while the volume discount applied. The regression shipped to production and affected 23 customers over 11 days before a support ticket triggered manual investigation. Remediation required refunding and re-charging 23 customers, a manual reconciliation with Stripe, and two engineer-weeks of test coverage work that the team should have done before the feature shipped. The coverage gate had not failed. The coverage gate had provided no signal whatsoever about the risk distribution in the codebase. Connect this to the test strategy decision record: the coverage gate and the test strategy are related but distinct decisions; the test strategy specifies what kinds of tests the team writes and at what granularity (unit, integration, end-to-end); the coverage gate specifies the minimum acceptable coverage from those tests; a coverage gate without a test strategy produces coverage that maximizes the metric using whichever test type is easiest to write, which on most codebases means unit tests with mocked dependencies — exactly the test type that produces high line coverage on utility and model code while leaving the orchestration code that integrates those utilities untested.
A 48-person developer tools company built a static analysis platform for enterprise engineering teams. Their frontend was a React application: a multi-step workflow interface for configuring analysis rules, reviewing findings, and managing suppression lists. The engineering team was proud of their test coverage — a React Testing Library suite with 100% line coverage on every component. The VP of Engineering had introduced the 100% requirement two years prior after inheriting a different frontend codebase at a previous company that had been extremely difficult to refactor because no tests existed. The 100% requirement was enforced strictly: any PR that added a new component file required tests achieving 100% coverage on that file before merging. Engineers new to the team were initially surprised by the requirement and then impressed by it. The test suite had over 400 test cases.
The tests were comprehensive on component rendering. Each component had tests that rendered it with all its required props, verified that the rendered output contained the expected elements, and simulated the expected user interactions (button clicks, form field changes) and verified that the component called the expected callback functions. The components were well-isolated: each test rendered the component in isolation using React Testing Library's render function, mocked all external state with hardcoded values, and asserted on the component's direct output. 100% line coverage was achievable on each component because all the component's branches — loading state, error state, empty state, populated state — were exercised in the test file. The tests were fast, deterministic, and passing. The coverage number was accurate.
What the 100% component-level coverage did not measure was the interaction between components as data flowed through the multi-step workflow. The workflow had five steps: rule configuration, target scope selection, scheduled analysis settings, notification routing, and review assignment. Each step was a separate component. The state shared between steps was managed in a parent workflow component using a combination of React state hooks and a custom context. The context's data flow had been refactored in a significant architecture change three months prior, when the team had migrated from a Redux store to a lighter-weight context model. The refactor had been correct — all component tests passed with the new architecture — but the integration between the rule configuration step (step 1) and the notification routing step (step 4) had a subtle dependency that the architecture change had broken: the notification routing component read the rule severity from the context to pre-populate the notification priority, and the migration had changed the context shape in a way that caused the severity to be undefined rather than the expected string value when the user navigated from step 1 to step 4 without visiting steps 2 and 3 in sequence.
The bug was not visible in component tests because each component's test mocked the context with a complete, correctly-shaped value. The component did the right thing with the right input. The integration test that would have caught it — render the full workflow, navigate from step 1 to step 4 skipping steps 2 and 3, assert that the notification priority was correctly pre-populated — did not exist because the coverage requirement was at the component level, not at the integration level, and the component-level requirement was satisfied. The bug shipped to production and was reported by two enterprise customers who discovered they could not configure notifications correctly for analysis rules they had created in a single session. The fix was 4 lines of code, found in one hour. The test that would have caught it was 12 lines, written after the fix. The 400-test suite with 100% coverage had not caught it. Connect this to the CI/CD pipeline decision record: the CI/CD pipeline design determines which test types run at which point in the delivery pipeline; a pipeline that runs only unit tests in the CI gate and skips integration tests because they are slower will catch unit-level regressions and miss integration-level regressions even when the unit test coverage is 100%; the coverage gate and the pipeline design are the same decision viewed from different directions: the coverage metric measures what the unit tests cover, and the pipeline design determines whether integration tests that would reveal what the unit tests do not cover are actually run before the PR merges or only after.
A 41-person B2B SaaS company built a contract lifecycle management platform for professional services firms. They had a mature engineering practice: Architecture Decision Records for major decisions, a code review policy, an 80% branch coverage requirement enforced in CI. The coverage requirement had been in place for two years and was well-understood by the team. The test suite was comprehensive, the coverage was real, and the team had genuine confidence in the test suite's ability to catch regressions. In Q3 of the company's fourth year, the company entered a critical sales cycle with a large customer whose contract represented 22% of annual recurring revenue. The deal had a hard deadline: the customer required a specific integration feature that the platform did not yet have, and the customer had given the company a 10-week window to ship it before signing the contract. The VP Engineering and the CTO had a conversation about the sprint priority and the coverage requirement. The conversation happened in a series of messages in a Slack channel and then in a 30-minute Zoom call. The outcome was a temporary suspension of the 80% branch coverage gate for the duration of the 10-week sprint. The reasoning: the integration feature required significant new code in a part of the codebase that would be difficult to test at the 80% level in the sprint timeline; the feature would be manually QA'd before the customer demo; the team would restore the gate after the deal closed and write the tests before the next major release. The decision was communicated to the engineering team via Slack. No ADR was written. The Zoom call was not recorded.
The deal closed in week 9. In week 11, the VP Engineering and the CTO had a separate conversation about restoring the coverage gate. The VP's position was that the gate should be restored with the current coverage as the new baseline rather than the original 80% threshold, because the sprint code was production-tested by the customer integration and writing tests for code that had been running in production for two weeks felt lower priority than the next sprint's features. The CTO's position was that the gate should be restored at 80% as originally planned. The conversation was inconclusive — the CTO agreed to the VP's proposal "for now" and the decision to restore the gate was deferred to the next planning session. The next planning session's agenda was full of feature work. The gate was not restored.
Six months after the original suspension, the team's new engineering manager — hired in month 4 of the suspension period — ran a coverage audit as part of a quarterly engineering health review. Aggregate branch coverage was 31%. The 10-week sprint code had 9% branch coverage. The modules most relied upon by the customer integration had 6% coverage. The team had not noticed because CI was not failing — the gate had been suspended, not just lowered — and no coverage trend was visible on any dashboard. The engineering manager estimated that writing the missing tests at the current backlog rate would require 6 engineer-weeks. The VP and CTO agreed the tests were necessary. The 6 engineer-weeks were not scheduled in the next sprint because feature work was higher priority. They were not scheduled in the sprint after that either. At the 12-month mark of the suspension, coverage was 27% and the originally-planned restoration had never happened. Three separate incidents in those 12 months had been traced to untested code paths in the sprint code. None of the incidents were catastrophic, but each had required manual investigation because the test suite provided no signal about where the regression was. Connect this to the code review policy decision record: a code review policy that requires human review of every PR but does not specify a coverage requirement creates a situation where reviewers must individually enforce coverage as a judgment call; when the coverage gate is suspended and the policy is silent on minimum coverage, reviewers have no clear standard to apply, and the path of least resistance is to approve PRs that are otherwise correct even when their test coverage is low; the coverage gate is the enforcement mechanism that removes the judgment call from individual reviewers by making the requirement automatic and objective; suspending the gate transfers the enforcement burden back to reviewers at the moment when the team's attention is most focused on other work, which is the moment when the burden is least likely to be applied consistently.
Structural properties set by the test coverage requirement decision
Three structural properties are determined by the founding decisions about which coverage metric to use as the gate, whether the threshold applies to aggregate coverage or to critical-path distribution, and what the enforcement and exception model looks like. None of these properties are visible in weekly sprint metrics or the coverage badge that appears on the repository homepage. They are set at the time the coverage requirement is established and become visible at the first regression that escapes production in a module with high aggregate but low critical-path coverage, at the first post-mortem that traces a defect escape to a test suite with 100% unit coverage and zero integration coverage, and at the first engineering health review that reveals the coverage gate was suspended six months prior and never restored.
Property 1: The regression detection confidence ceiling and the coverage distribution problem. The regression detection confidence ceiling is the maximum probability that the test suite will detect a regression in a given code path, determined not by the aggregate coverage percentage but by the coverage distribution across paths in the codebase. An aggregate coverage percentage — even a high one — provides no information about whether the specific code path that is about to regress is covered. The ceiling is invisible in the aggregate number and visible only when coverage is measured and enforced at the path or module level. A codebase with 90% aggregate coverage and 10% coverage on the payment processing module has a confidence ceiling of approximately 10% for detecting a regression in payment processing — the test suite will catch that regression in roughly 1 in 10 cases, regardless of what the aggregate number says. The distribution problem is structural rather than accidental: utility code, model validation, and serialization code — the code that is easy to test and that generates clean, meaningful unit tests — accumulates high coverage naturally because it is written by engineers who find testing it straightforward and who recognize it as testable. Orchestration code, integration code, and legacy code that coordinates between multiple systems accumulates low coverage because it is harder to test in isolation, requires more test setup, and is often written under time pressure by engineers who are focused on getting the coordination logic correct rather than on building a test harness for it. The distribution is not random — it systematically concentrates coverage on the code that is least likely to be the source of a regression in production and systematically leaves uncovered the code that coordinates between systems and handles edge cases in complex business logic. The aggregate metric averages across this distribution and produces a number that is accurate but not useful as a signal for whether the team's regression detection capability is actually protecting them. Connect this property to the observability sampling decision record: the coverage distribution problem and the sampling bias problem are structurally analogous — just as a telemetry sampling strategy that systematically undersamples slow requests produces an average latency metric that is accurate across the sample but misleadingly low because the tail is underrepresented, a coverage strategy that systematically undercounts coverage in orchestration and integration code produces an aggregate metric that is accurate across the codebase but misleadingly high because the paths most likely to contain production regressions are underrepresented; the mitigation in both cases is to measure and enforce at the path level rather than relying on aggregate metrics to represent the whole distribution.
Property 2: The test investment return failure mode and the metric incentive misalignment. The test investment return failure mode is the systematic pattern of test writing that emerges when aggregate line or branch coverage is the primary quality gate: engineers optimize for the number, not for the detection capability of the tests they write. Line coverage measures whether a line was executed at least once; it does not measure whether the assertion on that execution would fail if the line's behavior changed. A test that calls a function with one representative input and asserts the expected output contributes the same line coverage as a test that exercises the function at its boundary conditions, with invalid inputs, and in the presence of the error conditions that the function is designed to handle. When the gate is a coverage percentage, both tests pass the gate. The first test is faster to write and simpler to maintain. Engineers writing tests under sprint pressure will write the first test. The result is a test suite with high line coverage composed primarily of happy-path unit tests with mocked dependencies — a suite that is excellent at catching regressions in the lines it covers when the covered lines are invoked with the inputs the test uses, and completely blind to the regressions that actually occur in production: boundary conditions, error path handling, concurrency behavior, and the interactions between components that are each mocked away in unit tests. The metric incentive misalignment is not a failure of engineer judgment — it is a rational response to a metric that does not distinguish between high-value tests and low-value tests. The only metric that directly measures detection capability is mutation score: the percentage of deliberately introduced code mutations that are caught by at least one failing test. A test suite that catches 90% of mutations is a test suite that would catch 90% of the class of bugs that mutations simulate; a test suite that catches 30% of mutations is a test suite that would catch 30% of that class, regardless of what its line coverage number is. Connect this property to the security scanning decision record: the test investment return failure mode is structurally identical to the false positive problem in security scanning — an overly sensitive SAST configuration that generates 200 alerts per PR causes engineers to mark alerts as false positives without reviewing them, reducing the effective detection rate of the scanner to near zero even as the number of alerts remains high; a coverage gate that is satisfied by tests that maximize the metric rather than tests that maximize detection capability reduces the effective regression detection capability of the test suite toward zero even as the coverage number remains high; in both cases, the metric is being gamed not through bad intent but through rational optimization of the incentive the metric creates; the solution in both cases is to measure the actual signal (vulnerability detection rate in SAST, mutation score in testing) rather than the proxy metric (alert count in SAST, line coverage in testing), at least for the paths where the detection capability matters most.
Property 3: The coverage decay surface and the gate reinstatement failure mode. The coverage decay surface is the rate at which test coverage on existing code paths decreases over time as those paths evolve — distinct from the rate at which new untested code is added. A codebase where every new PR passes the coverage gate can still experience coverage decay on existing critical paths when the gate is defined as a percentage threshold on the PR delta (new code must have N% coverage) rather than as a ratchet on the whole-codebase total (the overall coverage number cannot decrease). The delta gate prevents new untested code from being added but does not detect when an existing tested function is refactored in a way that the existing tests no longer exercise the refactored branches — the test still passes because the refactored function's new behavior on the tested inputs is the same as the old behavior, but the refactored branches that handle the inputs the tests do not exercise are now untested. The gate reinstatement failure mode is a specific and predictable failure pattern: a temporary suspension of the coverage gate — always justified by a time-sensitive delivery — creates a new de facto baseline in the codebase's coverage that is lower than the original gate threshold; when the sprint ends and the team attempts to reinstate the gate at the original threshold, the gate would fail immediately because the sprint code is below threshold; the team must either restore the threshold by writing the deferred tests (which requires the engineer-weeks they deferred the tests to avoid spending) or lower the threshold to accept the current baseline (which normalizes the reduced coverage and permanently accepts the regression detection capability reduction that the temporary suspension created); neither option is attractive during active feature delivery, so the decision is deferred; the deferred decision is deferred again at the next planning session, and again at the next one, until the suspension is so old that the original threshold is no longer meaningful as a target and the team negotiates a new lower threshold that they can achieve without retroactive test work. The decay surface is not primarily a technical problem — the technical mechanism (add tests, run coverage, restore gate) is straightforward. It is an incentive problem: the work to restore the coverage gate has no direct feature value, takes engineer-weeks, competes with feature work in sprint planning, and will be invisible to users when it is done. Connect this property to the technical debt decision record: the coverage debt created by a suspended gate is the most tractable form of technical debt because its location is precisely measurable (the CI coverage report identifies exactly which files are below threshold) and its remediation is mechanical (write tests for the identified files); it is also the most persistently deferred form because the measurability that makes it tractable also makes it possible to track the debt number with precision without acting on it, and the sprint planning process consistently prioritizes work with direct user value over work whose value is the invisible prevention of future regressions in paths that have not yet regressed.
The test coverage requirement ADR: five sections
Section 1: Coverage metric selection and what it measures. The metric selection section should specify which coverage metric is used as the gate, why it was selected over alternatives, and what the team's explicit understanding is of what the metric does and does not predict about regression detection capability. The choice between line coverage, branch coverage, and mutation score has material consequences for the failure modes the gate prevents. Line coverage is the default in most coverage tools (Jest, Istanbul, coverage.py, SimpleCov) and the easiest to achieve high percentages on, but it is the least predictive of branch-level and boundary-condition regression detection because it does not require that both sides of conditionals be exercised. Branch coverage requires that every conditional branch be taken at least once, which forces tests to exercise the error paths and edge cases that line coverage allows to be skipped; it is harder to achieve high branch coverage percentages on untested legacy code but more predictive of whether the test suite would catch a regression in the error handling that typically causes production incidents. Mutation score is the most accurate predictor of detection capability but is computationally impractical as a per-PR CI gate on large codebases — the practical model is to run mutation testing on critical paths only, on a scheduled cadence (weekly on the critical-path module set), not as a per-PR gate. The metric selection section should include the reasoning for the choice — not just the chosen metric but why line coverage was rejected in favor of branch coverage (or why line coverage was chosen despite its limitations), what the team understands about the metric's blind spots, and under what conditions the metric selection should be revisited (a production incident in a module with high coverage is the clearest trigger for reconsidering whether the metric is providing accurate confidence). Connect this section to the test strategy decision record: the test strategy specifies which test types the team writes — unit tests, integration tests, end-to-end tests — and the coverage metric selection is downstream of the test strategy; branch coverage on unit tests with mocked dependencies is a different metric than branch coverage on integration tests that exercise the actual data flow between components; the coverage gate should specify which test types are included in the coverage measurement (typically unit and integration, not end-to-end) and what the coverage tool's module-include and module-exclude configuration is so that the metric is measuring the code the team intends to measure rather than the aggregate of test utilities, generated code, and vendor libraries that Istanbul's default configuration includes.
Section 2: Threshold model for new code, existing code, and critical paths. The threshold section should specify three distinct thresholds that apply in different contexts, with the reasoning for each number. For new code (the PR delta threshold): the minimum branch coverage that new production code must have before merging, expressed as a per-PR delta gate — 80% branch coverage on lines added or modified in the PR is a practical threshold that blocks the worst cases (entirely untested new functions) without blocking PRs where some defensive branches are legitimately low-probability and expensive to test. For existing code (the ratchet rule): the whole-codebase coverage cannot decrease below its current level — a PR that reduces overall coverage by removing tests or by adding significant untested code that outweighs the tested code in the PR will fail the ratchet gate even if the PR delta itself passes the new-code threshold; the ratchet prevents coverage decay without requiring retroactive test work, because it only fires when a specific PR is the cause of a measurable decrease. For critical paths (the critical-path threshold): the specified set of critical-path modules must maintain a higher threshold — 90% branch coverage — independent of the aggregate, enforced by a separate CI step that measures coverage on the critical-path module set specifically and fails if the threshold is not met; this is the gate that would have caught the payment module's 12% coverage in the first failure pattern above, because it would have required the team to measure and gate on the payment module's coverage independently of the utility code coverage that was inflating the aggregate. The reasoning for each number should be explicit: 80% on new code because lower thresholds allow large untested functions to merge; 90% on critical paths because the regression detection confidence ceiling for payment and authentication code should be high; ratchet on existing code because retroactive coverage requirements are impractical and the ratchet prevents decay without requiring a coverage sprint. Connect this section to the CI/CD pipeline decision record: the threshold model determines which CI steps fail on which thresholds, and the pipeline design determines whether those CI steps are required or advisory; a threshold model that is implemented as an advisory report rather than a blocking gate is a threshold model that produces no enforcement — engineers will note the advisory and continue the merge, and the coverage will not improve; the enforcement model (blocking gate or advisory) is as important as the threshold itself and should be specified explicitly in the decision record.
Section 3: Scope and exclusion policy. The scope section specifies which code is included in coverage measurement and which is excluded, and the process for maintaining the exclusion list as the codebase evolves. The default coverage tool configuration on most codebases measures coverage including test files, generated code, database migration files, configuration files, and vendor library code — all of which inflate the coverage number without representing the production code quality the gate is intended to measure. The exclusion list should specify at minimum: test utility files and test fixtures (these are always excluded — testing the tests is circular), auto-generated code (OpenAPI-generated client stubs, Prisma-generated client, protobuf-generated types — these are not maintained by the team and cannot be meaningfully tested at the source level), database migration files (migrations are executed by the migration runner, not exercised by the unit test suite, and including them in coverage deflates the aggregate without providing useful signal), and vendor or third-party code vendored into the repository. Beyond these baseline exclusions, the scope section should specify whether the coverage measurement includes integration test coverage, end-to-end test coverage, or unit test coverage only — most coverage tools can be configured to collect coverage from multiple test types and aggregate them, which produces a more accurate picture of what is actually tested but also requires deliberate configuration to ensure that end-to-end test coverage (which may cover many lines in a single test run) does not inflate the number in ways that mask the unit test coverage gaps. The exclusion list should be maintained in a configuration file committed to the repository (Jest's coveragePathIgnorePatterns, pytest's omit configuration, SimpleCov's add_filter) and reviewed when the codebase structure changes significantly — a new major subsystem or a significant refactor may require the exclusion list to be updated to reflect the new module boundaries. Connect this section to the code review policy decision record: the scope exclusion list is a configuration file that should be reviewed as part of the standard PR review process, but because it is a configuration file rather than a test file or production code file, reviewers may not scrutinize it closely; the coverage scope configuration file should be explicitly called out in the code review policy as requiring review from the tech lead or designated test strategy owner when modified, because a change to the exclusion list that removes a critical-path module from measurement can drop the effective coverage gate below the intended threshold without triggering any CI failure.
Section 4: Enforcement model and exception process. The enforcement section specifies whether the coverage gate blocks merges in CI or generates a report only, what the PR failure message communicates to engineers (not just "coverage below threshold" but which specific files are below threshold and by how much), and the exception process for PRs that need to merge with coverage below the threshold. Blocking gates are more effective than advisory reports at maintaining coverage levels because they remove the judgment call from individual reviewers — the gate either passes or fails, and a failing gate blocks the merge regardless of reviewer approval. Advisory reports require each reviewer to make an individual judgment about whether the coverage gap is acceptable for this specific PR, which produces inconsistent enforcement and gradually normalizes lower coverage levels. The exception process should specify how an engineer requests a time-bound exception (merge a PR below threshold to unblock a critical delivery), who approves the exception (tech lead or VP Engineering, not the PR reviewer), what the maximum exception duration is (14 days, after which the tests must be written or the exception must be formally renewed with updated reasoning), and how exceptions are tracked (a Jira ticket or GitHub issue for each exception, so the exception log is visible in the sprint planning backlog rather than in a Slack thread that gets buried). The exception process design is the mechanism that determines whether temporary suspensions become permanent reductions. An exception process with clear time limits and a tracking mechanism creates a recovery path: the exception is finite, the tracking ticket is in the sprint backlog, and the closure criterion (tests written, gate restored) is explicit. An exception process that is a Slack message from a VP saying "let's suspend the gate for this sprint" has no recovery path: there is no ticket to resolve, no deadline that triggers a review, and no actor who is responsible for reinstating the gate when the sprint ends. Connect this section to the technical debt decision record: the exception process for the coverage gate should follow the same tracking model as other technical debt — a ticket with a specific owner, a deadline, and a clear closure criterion; the coverage exception is not a sprint-level decision about whether to write tests, it is a technical debt creation event that requires the same management as any other deliberate accumulation of debt; engineering teams that treat the coverage exception as a sprint-level call and teams that treat it as a technical debt creation event with a formal tracking requirement have very different long-term coverage trajectories, and the difference is entirely in the process that governs the exception, not in the team's values or engineering culture.
Section 5: Coverage review cadence and test quality signals. The review cadence section specifies how often coverage is reviewed at the distribution level rather than just the aggregate, what signals indicate that high-coverage code has low regression detection capability, and how the team responds when a production incident occurs in a module with high coverage. Coverage should be reviewed at the distribution level quarterly: not just the aggregate percentage, but which modules are below the critical-path threshold, which modules have been on a downward trend over the past quarter, and which recent incidents occurred in modules with above-threshold coverage (indicating that the coverage metric is not accurately representing detection capability in those modules). The test quality signal is a production incident in a module with high coverage: when a regression escapes to production in a module with 85% branch coverage, the correct response is not "the coverage metric failed us" but "which tests exist in this module, and what class of regressions do they not catch?" — an investigation that typically reveals that the existing tests cover the happy path comprehensively and cover the error paths minimally, and that the mutation score on the module's tests is significantly lower than the branch coverage number. Mutation score on critical-path modules should be measured on a scheduled cadence (weekly or at release time) as a secondary signal that does not gate PRs but provides the test quality information that the coverage gate cannot: if the payment processing module has 82% branch coverage and 28% mutation score, the test suite is not protecting the payment path regardless of what the coverage badge says, and the quarterly coverage review should flag this as a test investment priority. Connect this section to the open-source extractor: the test coverage requirement decision — which metric, which threshold, which critical paths, what the exception process looks like — is a decision that engineering teams make in conversations: a CTO and VP Engineering on a Zoom call debating whether 80% line coverage is sufficient or whether branch coverage is worth the friction of requiring it; a discussion in the engineering planning meeting about whether to suspend the gate during the growth sprint; a Slack thread where the team debates whether the mutation score finding on the payment module warrants a two-week test sprint; the written outputs of these conversations are the CI configuration file that specifies the threshold and the Jira ticket that is never opened for the suspended gate; the reasoning behind the threshold choice, the exception decision, and the mutation score finding — the reasoning that determines whether the next generation of engineers raising the same questions in their own Zoom calls will repeat the same decisions or make different ones — lives in the chat history that preceded the CI configuration change, which is the context that WhyChose's extractor recovers from AI chat export history and surfaces as a searchable decision record.
FAQ
What is the difference between line coverage, branch coverage, and mutation score for a coverage gate?
Line coverage measures whether each line was executed at least once during the test run. Branch coverage measures whether each conditional branch — the true and false paths of every if statement, ternary, and switch case — was executed at least once. Mutation score measures what fraction of deliberately introduced bugs in the codebase are caught by the test suite — a mutation is caught when a test that was previously passing now fails after the mutation is introduced. Line coverage is easiest to achieve high percentages on and least predictive of regression detection capability: a single test with one representative input can produce 100% line coverage on a function with ten conditional branches, leaving nine branches untested. Branch coverage requires that both sides of every conditional be exercised, providing significantly better protection against regression in error-handling code, at the cost of being harder to achieve high percentages on legacy untested code. Mutation score is the most accurate predictor of regression detection capability because it directly measures whether the test suite catches a class of real bugs, but it is computationally expensive and impractical as a per-PR CI gate on large codebases. The practical recommendation is to gate on branch coverage for new code, use line coverage as a baseline for legacy code, and run mutation testing periodically on critical paths to measure detection capability independently of the coverage metric.
How should coverage thresholds differ for new code, existing code, and critical paths?
New code, existing code, and critical paths require different threshold models. For new code, the threshold should be a minimum on the PR delta: 80% branch coverage on lines added or modified in the PR prevents entirely untested new functions from merging without requiring 100% coverage that would block legitimate PRs where some branches are extremely defensive and expensive to test. For existing code, applying the new-code threshold retroactively is impractical — it would block all PRs touching legacy modules until the entire module is brought up to standard, causing engineers to avoid touching legacy code. The practical model for existing code is a ratchet: the whole-codebase coverage number cannot decrease below its current level. For critical paths — payment processing, authentication, billing calculation, data export — a separate CI step should measure coverage on the critical-path module set specifically and apply a higher threshold (90% branch coverage) independent of the aggregate, because a regression in these paths has higher customer impact and the investment in maintaining higher coverage is proportional to that impact.
What triggers a test coverage gate suspension, and how should the exception process work?
A test coverage gate should be suspended only when a specific delivery has a hard external deadline that cannot be moved and where the tests genuinely cannot be written within the delivery timeline. The exception process should require: a named owner who is responsible for writing the missing tests after the delivery (not 'the team' — a specific engineer), a written ticket in the sprint backlog with a specific deadline (14 days from the delivery date is a practical maximum), the VP Engineering or CTO's approval rather than the PR reviewer's, and a CI configuration change that lowers the threshold to the current baseline rather than a complete suspension (lowering to the baseline allows new PRs to continue merging while making the threshold specific and trackable). Without a specific owner, deadline, and tracking ticket, a temporary suspension becomes permanent: no one is responsible for reinstating the gate, the deadline passes unnoticed, and the team's new de facto standard is the lower coverage level that accumulated during the suspension. The tracking ticket is the recovery mechanism. Without it, the suspension is a permanent reduction dressed as a temporary one.
What should a test coverage requirement decision record specify?
Five specifications. First, coverage metric selection: which metric is used as the gate (branch coverage recommended over line coverage), why it was chosen, and what the team understands about its blind spots. Second, threshold model: the PR delta threshold for new code, the ratchet rule for whole-codebase coverage, and the higher threshold applied to explicitly named critical-path modules. Third, scope and exclusion policy: which directories and file patterns are excluded from coverage measurement, how the exclusion list is maintained and reviewed, and whether integration test coverage is aggregated with unit test coverage. Fourth, enforcement and exception process: blocking gate vs advisory report, the exception approval and tracking process, maximum exception duration, and the closure criterion that determines when the exception is resolved. Fifth, review cadence and test quality signals: quarterly coverage distribution review at the module level, mutation score measurement on critical paths on a scheduled basis, and the protocol for investigating a production incident in a module with above-threshold coverage.
Further reading
- Test strategy decision record — the coverage requirement is downstream of the test strategy: the test strategy specifies which test types the team writes and at what granularity, and the coverage gate measures coverage from those tests; a branch coverage gate on a test suite composed entirely of unit tests with mocked dependencies produces different regression detection capability than the same branch coverage gate on a test suite that includes integration tests; the test strategy and the coverage requirement should be specified together, with the coverage threshold explicitly derived from the test types in scope.
- CI/CD pipeline decision record — the CI/CD pipeline design determines which test types run at which point in the delivery pipeline and whether the coverage gate is a blocking step or an advisory report; a pipeline that runs only unit tests in the CI gate and skips integration tests because they are slower will catch unit-level regressions and miss integration-level regressions even when the unit test coverage is 100%; the coverage gate and the pipeline design are the same decision viewed from different directions.
- Code review policy decision record — the coverage gate automates the enforcement that code review would otherwise apply inconsistently; a code review policy that specifies test coverage as a review requirement without a CI enforcement mechanism depends on each reviewer making an individual judgment about whether coverage is adequate for the specific PR, which produces inconsistent enforcement and gradually normalizes lower coverage levels; the coverage gate removes the judgment call from individual reviewers and makes the requirement objective and automatic.
- Technical debt decision record — coverage debt created by a suspended gate is the most tractable form of technical debt because its location is precisely measurable (the coverage report identifies exactly which files are below threshold) and its remediation is mechanical (write tests for the identified files); it is also the most persistently deferred form because measurability enables tracking without acting, and sprint planning consistently prioritizes work with direct user value over work whose value is invisible prevention of future regressions.
- Open-source extractor — the test coverage requirement decision — which metric to gate on, what threshold applies to which scope, how the exception process works — is a decision that engineering teams make in conversations: a CTO and VP Engineering debating whether branch coverage is worth the friction on a Zoom call, a Slack thread discussing whether to suspend the gate during a growth sprint, a planning meeting where the mutation score finding gets noted and deferred; the written output is the CI configuration file and a Jira ticket that may never be opened; the reasoning behind the threshold choice and the exception decision lives in the chat history that preceded the configuration change, which is the context WhyChose's extractor recovers from AI chat export history.