skip to content

Why does `go test` report 0% coverage for a package that another package's tests exercise?

level: middleimportance: should knowfreq 42%

answer

  1. instrumentation is per-package
  2. the dependency was compiled unmodified
  3. one flag takes a package pattern
  4. -coverpkg widens what is instrumented
  5. widening also moves the denominator

basics

~20 s

By default go test instruments only the package being tested, so calls into other packages are never counted. Pass -coverpkg with a package pattern, for example -coverpkg=./..., to instrument those packages too and credit cross-package execution.

solid answer

~40 s

Coverage instrumentation is per-package and, by default, `go test` instruments only the package under test. So an end-to-end test living in its own package can drive your whole service and still credit nothing to the packages it calls — they were compiled unmodified, with no counters in them. The fix is `-coverpkg`, which takes a package pattern list: `go test -coverpkg=./... -coverprofile=cover.out ./e2e` instruments everything matched by `./...` into that test binary, so the profile now contains blocks from every package the test touched. The costs are real: every test binary now links instrumented copies of every listed package, so builds are slower and binaries larger, and the denominator becomes all of `./...`, so packages nothing exercises drag the printed percentage down.

code

text · 6 lines
text
# only ./e2e itself is instrumented; the packages it drives are not
go test -coverprofile=cover.out ./e2e

# every package matching ./... is instrumented into the e2e test binary
go test -coverpkg=./... -coverprofile=cover.out ./e2e
go tool cover -func=cover.out

go deeper

for a junior

Know that coverage is measured per package and that a package with no test files of its own will show nothing, even if other tests call into it.

for a middle

Explain that only the package under test is instrumented by default, name -coverpkg as the flag that widens instrumentation, and describe the pattern syntax it takes.

for a senior

Demonstrate judgment about the cost: instrumented copies land in every test binary, the denominator changes so thresholds must be re-derived, and a narrow pattern often beats ./... .

for a principal

Own what the pattern should be for the organisation — which packages belong in the measured set at all, and whether integration suites are allowed to count toward the same number as unit tests.

## The default is narrower than people assume When you run `go test -cover ./...`, the toolchain builds one test binary per package and instruments **only that package's own source**. The dependencies it links are ordinary, uninstrumented builds. The percentage printed next to each package therefore answers a specific question: *how much of this package's code did this package's own tests run?* That default is right for unit tests and wrong for the two situations people hit most: 1. **An integration or end-to-end test package.** You have `./e2e` containing only `_test.go` files that start your server and hit it over HTTP. Its own package has almost no statements, and every package it exercises is uninstrumented, so nothing is credited anywhere. 2. **A package with no tests of its own.** `internal/store` is covered thoroughly by the handler tests in `internal/api`, but `go test ./...` shows it with no test files and no coverage. In both cases the code really did run. It just was not instrumented, and coverage can only report on instrumented code. ## What -coverpkg does `-coverpkg` takes a comma-separated list of package patterns and says: *instrument these packages, whichever test binary is being built*. ``` go test -coverpkg=./... -coverprofile=cover.out ./e2e ``` Now the `./e2e` test binary links instrumented copies of every package matching `./...`, and the resulting profile contains their blocks. `go tool cover -html=cover.out` will show your `internal/store` source annotated even though `internal/store` has no test file at all. You can also aim it narrowly: ``` go test -coverpkg=example.com/m/internal/store ./internal/api ``` which credits only the store package for whatever the API tests drive through it. ## What it costs, and what it changes about the number - **Build time and binary size.** Instrumented copies of the listed packages get compiled into *every* test binary in the run. On a large repo, `-coverpkg=./...` across `./...` is a noticeably heavier build than the same run without it — the cross-product is the reason. - **The denominator moves.** Without `-coverpkg`, each package's percentage is relative to itself. With it, the percentage `go test` prints for each package is relative to the whole instrumented set, and the output line says so — it reports coverage of statements in the pattern you named. A unit test that fully covers its own package can print a small number, which surprises people reading CI logs. - **Untouched packages count against you.** `./...` sweeps in `main` packages, generated code, and helper packages nothing exercises. They contribute statements to the denominator and zero to the numerator. - **Merging still matters.** Running `go test -coverpkg=./... -coverprofile=cover.out ./...` in one invocation gives a single profile covering everything; splitting the run across several invocations gives you several profiles that `go tool cover` cannot merge. ## Choosing the pattern deliberately The useful discipline is to make `-coverpkg` name what you actually want measured rather than reflexively writing `./...`. If the goal is 'how much of our business logic do our tests reach', a pattern like `./internal/...` excludes `cmd/` entry points and generated packages, and the number stops moving every time somebody adds a main package. If the goal is 'this one integration suite proves the store layer is exercised', name the store package alone and read the result as evidence about that suite rather than as a repository-wide score. ## The interview shape The question is usually posed as a puzzle: *our end-to-end tests clearly run this code, so why is it 0%?* The answer has two halves — instrumentation is per-package and defaults to the package under test, and `-coverpkg` is the flag that widens it — and a good candidate volunteers the third half unprompted: widening it changes the denominator and the build cost, so the number you get afterwards is not comparable with the number you had before.

  • Why does -coverpkg=./... make a large repository's test run noticeably slower?
    Because the listed packages are instrumented into every test binary the run builds, not just once. Each binary links its own instrumented copies, so build work multiplies with the number of test packages, and every instrumented block also costs a counter update at run time.
  • After adding -coverpkg=./..., a package's printed percentage dropped sharply. Is that a regression?
    No — the denominator changed. Without -coverpkg each package is scored against its own statements; with it, the run is scored against every statement in the named pattern, including packages nothing exercises. The two numbers are not comparable, so any threshold has to be re-derived rather than carried over.
  • How would you credit only one dependency rather than the whole module?
    Give -coverpkg that package's import path instead of ./..., for example -coverpkg=example.com/m/internal/store. Only that package is instrumented into the test binary, the build stays cheap, and the resulting profile answers a narrow question: what did this suite actually drive through the store layer?

Only the rooms you wired with motion sensors report footsteps. Walking through the unwired corridor leaves no trace, however many times you did it.

saying these in an interview costs you the question

  • Concludes the code never ran because coverage shows zero
  • Thinks dependencies are instrumented automatically
  • Adds -coverpkg=./... and keeps the old threshold
  • Believes -coverpkg is free at build time
  • Confuses -coverpkg with the list of packages to test