Stage 7 · Master
Testing Fieldwork
Coverage & CI Gates
Measure coverage as a guardrail, not a vanity score, and let CI block merges only on signals that actually predict regressions.
Why Coverage Needed a Policy, Not a Badge
Coverage numbers are dangerously easy to admire for the wrong reason. A dashboard that says 92% looks healthy right until you realize the uncovered 8% contains the only transaction rollback path that matters. I did not want coverage to become that kind of decorative metric in Fieldwork. The point of measuring it was to force some minimum testing discipline, not to create a leaderboard.
That meant the first design question was not what percentage sounds impressive. It was what kind of behavior we wanted CI to block. A backend course repo like Fieldwork needs gates strong enough to prevent obviously under-tested merges, but not so rigid that engineers start gaming the metric with meaningless assertions just to satisfy a number. A bad threshold trains bad tests.
| Coverage policy | Why it sounds appealing | Why Fieldwork avoided it |
|---|---|---|
| 100% global coverage | Looks uncompromising | Encourages trivial tests and punishes low-value branches equally |
| No enforced threshold | No friction in CI | Allows testing discipline to erode silently |
| Moderate minimum plus targeted review | Blocks obvious neglect while preserving judgment | Requires humans to read beyond the number |
Picking a Threshold That Discourages Theater
Fieldwork uses coverage as a floor, not as a finish line. The threshold I would enforce for the Go backend is moderate enough that teams cannot ignore tests, but low enough that nobody is rewarded for writing nonsense assertions around getters or impossible branches. In practice, that means aiming around the 70-80% band for core packages, while relying on code review to ask whether the important behaviors are the ones actually covered.
I rejected a single moralized number like 95% because different packages carry different testing shapes. A repository package with a handful of query methods may earn higher meaningful coverage than a transport package with error wrappers and pass-through glue. The gate should signal neglect, not erase context. A thoughtful 74% can be healthier than a padded 91%.
A line can be covered by a weak test that never asserts the important branch outcome. Treat coverage as one signal in the release discipline, not the definition of confidence.
Wiring go test into a Real CI Gate
The CI gate itself should be brutally simple: run go test with coverage, collect the profile, derive the total percentage, and fail the job if it drops below the agreed floor. No manual spreadsheet. No copy-paste into a badge site. If a merge can weaken testing guarantees, that decision belongs in automation. For Fieldwork, the same workflow can run unit tests on every pull request and the heavier integration suite either in the same pipeline or as a required companion job.
I prefer small shell glue around go test over a complex third-party coverage platform for this stage of the project. The build should be understandable from the repository itself. Anyone reviewing the workflow ought to see exactly which packages are tested, how the threshold is calculated, and why the job failed. Fancy reports are optional. A transparent gate is not.
name: ci
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Run unit tests with coverage
run: go test ./... -covermode=atomic -coverprofile=coverage.out
- name: Enforce minimum coverage
run: |
total=$(go tool cover -func=coverage.out | awk '/total:/ {print substr($3, 1, length($3)-1)}')
minimum=75.0
awk -v total="$total" -v minimum="$minimum" 'BEGIN { exit !(total+0 >= minimum+0) }'
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
name: coverage.out
path: coverage.out
integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Run repository integration tests
run: go test ./... -run 'Test.*Repository|Test.*Rotation'The key move is that the workflow fails in CI, not in someone's memory. The threshold is visible, versioned, and reviewable like any other piece of engineering policy.
What Coverage Still Cannot Tell Us
Even with a gate, coverage misses whole categories of risk. It does not know whether the test asserted the right thing. It does not know whether a happy-path integration test skipped the concurrency branch that matters for refresh token rotation. It does not know whether the tenant isolation bug lives in a SQL clause reached by exactly one test but never scrutinized. Those are judgment calls that still belong to engineers and reviewers.
That is why Fieldwork treats low coverage as a merge blocker but high coverage only as a prompt for a second question: coverage of what? When a risky package changes — auth, pagination queries, RBAC middleware — reviewers should still look for targeted tests around the interesting branches. A threshold without human review invites theater. Human review without a threshold invites drift. The combination is the point.
#!/usr/bin/env bash
set -euo pipefail
go test ./... -covermode=atomic -coverprofile=coverage.out
summary=$(go tool cover -func=coverage.out | awk '/total:/ {print $3}')
echo "total coverage: ${summary}"
Local scripts are convenience only. The real enforcement still belongs in CI so that every branch is judged by the same rule.
The Gates Fieldwork Enforces
Fieldwork enforces go test in CI, records coverage with an explicit floor, and treats integration-heavy packages as required parts of the merge signal rather than optional extras. The threshold is moderate on purpose: high enough to block obvious neglect, low enough to avoid rewarding meaningless test inflation.
We rejected both extremes: no coverage gate at all and a vanity-chasing near-100% mandate. The first lets discipline decay; the second encourages theater. The better policy for this backend is a visible floor, honest review of high-risk changes, and CI automation that blocks merges when the basics are not there.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.