skip to content

Your CI gate fails a PR when `go tool cover -func` reports under 80%. How do you decide what that number should measure?

level: principalimportance: should knowfreq 30%

answer

  1. scope before threshold
  2. what is in the denominator
  3. where did the integration numbers go
  4. whole tree, package, or diff
  5. who may waive it, and on what evidence

basics

~20 s

Decide the measured set before the threshold: which packages -coverpkg instruments, whether end-to-end counters from GOCOVERDIR merge in, and whether the gate scores the repository total or only changed code. The number is only defensible once its scope is written down.

solid answer

~50 s

The digit is the least interesting part of the decision; the scope behind it is what you own. Concretely: does the gate run `go test ./...` and read each package's own percentage, or `-coverpkg=./...` so cross-package hits count and generated `cmd/` packages join the denominator? Do integration counters collected through `go build -cover` and `GOCOVERDIR` get merged with `go tool covdata` before the total is computed, or does the number silently exclude the suite that exercises the most code? Is the gate on the repository total — which a large repo can barely move, so the threshold stops discriminating — or on the changed lines in the diff, which the toolchain does not compute for you but the profile's file-and-line blocks make possible? I write those choices down, set the threshold from the current measured baseline rather than a round number, define who can waive it, and revisit it when it starts rewarding tests that execute code without asserting anything.

go deeper

for a junior

You are not expected to set the policy, but know which command produced the number your CI job prints and that it can be scoped to different sets of packages.

for a middle

Be ready to explain how the flags change the number: -coverpkg moves the denominator, the profile carries a mode header, and go tool covdata is what folds integration runs into the same total.

for a senior

Show that you would investigate scope before arguing about the digit, and that you can distinguish an uncovered path from an unmeasured one when a total drops.

for a principal

Own the policy end to end: the measured set, the baseline the threshold is derived from, whether the gate blocks or reports, who may waive it, and the evidence that would make you lower or retire it.

## Why this is a scope decision, not a threshold decision Two teams can both claim '80% coverage' from the same repository and be measuring different things by a factor of two. Before defending a number, you have to be able to state precisely what the Go toolchain was asked to count. ### Which packages are instrumented Without `-coverpkg`, `go test ./...` scores each package against its own statements, using only its own tests. That number is honest about unit testing and blind to everything an integration suite does. With `-coverpkg=./...`, every package matching the pattern is instrumented into every test binary: cross-package execution is credited, but `main` packages, generated code and helper packages nothing exercises enter the denominator and drag the total down. Neither is wrong; picking one and writing it down is what makes the threshold comparable over time. A middle position — `-coverpkg=./internal/...` — often measures what the team actually cares about and stops the number moving whenever somebody adds an entry point. ### Whether integration coverage counts If the service is exercised mainly by an end-to-end suite, a gate that reads only `go test` numbers is measuring the smaller half. Folding the other half in means building the server with `go build -cover`, running it with `GOCOVERDIR` set, merging the counter directories with `go tool covdata merge`, converting with `covdata textfmt`, and reporting on the merged profile. That is real pipeline complexity, and it brings a failure mode with it: a killed process writes nothing, so the total silently drops and the team responds by writing tests for code that was already covered. If you take this on, the harness must fail loudly when the counter directory is empty. ### Total, per-package, or changed lines A repository-wide total is the easiest thing to compute and the weakest gate: on a large codebase a single PR cannot move it, so the threshold neither blocks bad changes nor guides good ones. A per-package floor is sharper but produces noise on small packages, where one uncovered error branch is ten percentage points. A changed-lines gate — did this diff's new statements get executed — is the one that actually changes behaviour, and the Go toolchain does not provide it: you get a profile with `file:startLine.col,endLine.col` blocks and have to intersect it with the diff yourself. That is a build-and-own decision with a maintenance cost attached. ### The mode, and the pipeline's consistency If CI runs with the race detector, the profile will be `mode: atomic` whether or not anyone chose that. Any script that post-processes profiles must read the header rather than assume. Mixing modes or mixing `-coverpkg` scopes across jobs and then comparing the numbers is how a gate quietly stops meaning anything. ## The decision I would actually defend 1. **Name the measured set explicitly** in the CI configuration and in a short document: the `-coverpkg` pattern, whether integration counters merge in, and the mode. Anyone reading a failed gate should be able to see what was counted. 2. **Derive the threshold from the current baseline**, not from a round number. If the measured set sits at 71%, the gate goes at 71% with a ratchet, not at 80% with three months of grinding. 3. **Gate the diff where you can afford to build it**, and keep the repository total as a reported trend rather than a blocker. 4. **Define the waiver.** Someone must be able to merge with a documented reason — a generated package, a spike, an incident fix — and the waiver must be visible rather than a silently disabled job. 5. **Agree what would make you lower it.** The honest failure mode of any threshold is tests written to execute code rather than to check it. Coverage counts execution; nothing in the profile knows whether an assertion followed. When reviewers start seeing tests whose only purpose is the gate, the gate has begun costing more than it returns, and the person who set it owns saying so. ## What a strong answer sounds like It refuses to argue about 80 versus 85 and instead asks what is in the denominator, where the integration numbers went, and whether the gate is on the diff or the whole tree. It names the concrete flags that decide each of those, acknowledges the pipeline cost of the honest option, and ends with the organisational half: who set the number, who can override it, and what evidence would justify changing it.

  • A teammate proposes switching the gate to -coverpkg=./... to make the number look better. What do you tell them?
    That it changes the denominator rather than improving anything: cross-package hits now count, but every main package, generated file and unexercised helper joins the total, and build time rises because instrumented copies land in every test binary. If we adopt it, the threshold is re-derived from the new baseline and old numbers are not comparable.
  • How would you gate on changed lines when the Go toolchain has no such flag?
    The profile gives you `file:startLine.col,endLine.col` per block, so intersect those ranges with the added lines in the diff and compute a number over just that set. It is a small piece of tooling the team then owns and maintains — worth it if the gate is meant to change behaviour, not worth it if a reported trend would do.
  • Your end-to-end coverage silently stopped merging in and the total fell 12 points. How do you prevent a repeat?
    Treat empty measurement as a build failure, not as a low score: assert the GOCOVERDIR directory is non-empty after the run, check the instrumented binary's stderr for the warning about data not being written, and make teardown send SIGTERM and wait rather than SIGKILL. A gate that cannot distinguish 'not covered' from 'not measured' is dangerous.

saying these in an interview costs you the question

  • Argues about the threshold before defining the measured set
  • Adopts -coverpkg=./... while keeping the old threshold
  • Treats a missing integration profile as low coverage
  • Sets a repository-wide total gate on a huge codebase
  • Has no waiver path, so the job gets disabled instead
  • Compares numbers produced with different -coverpkg scopes