Understanding Test Coverage Gates
What Is a Test Coverage Gate in Simple Terms
A coverage gate is a contract: every PR must maintain at least X% test coverage, or it cannot merge. Without a gate, test coverage silently erodes. Engineers under time pressure skip tests, coverage drops from 85% to 70% to 50%, and eventually the test suite is so incomplete it provides no meaningful safety net.
With a gate, the decline is automatic: the pipeline fails, the PR cannot merge, and the engineer must either write the tests or refactor the code to be covered by existing tests.
How It Works
+------------------------------------------+| Test runner executes test suite || Instruments code to track execution |+------------------------------------------+ | v+------------------------------------------+| Coverage report generated || Lines covered: 847 / 1052 = 80.5% || Branches covered: 234 / 310 = 75.5% || Functions covered: 198 / 220 = 90.0% |+------------------------------------------+ | compare to threshold / \ meets below threshold threshold | | v v +----------+ +------------+ | Pipeline | | Pipeline | | PASSES | | FAILS | +----------+ | PR blocked | +------------+Coverage types:
Line coverage: % of code lines executed by at least one testBranch coverage: % of if/else branches tested (both true and false)Function coverage: % of functions called by at least one testStatement coverage: % of individual statements executed Line coverage is the most common gate.Branch coverage is stricter -- catches untested else branches. Recommended minimums: Line coverage: 80% Branch coverage: 70% Function coverage: 85%Coverage gate in GitHub Actions (Node.js):
coverage-gate: runs-on: ubuntu-latest needs: unit-test steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: name: coverage-report - name: Check coverage thresholds run: | LINES=$(jq '.total.lines.pct' coverage/coverage-summary.json) BRANCHES=$(jq '.total.branches.pct' coverage/coverage-summary.json) FUNCTIONS=$(jq '.total.functions.pct' coverage/coverage-summary.json) echo "Lines: $LINES%" echo "Branches: $BRANCHES%" echo "Functions: $FUNCTIONS%" fail=false (( $(echo "$LINES < 80" | bc -l) )) && { echo "::error::Line coverage $LINES% below 80%" fail=true } (( $(echo "$BRANCHES < 70" | bc -l) )) && { echo "::error::Branch coverage $BRANCHES% below 70%" fail=true } [ "$fail" = true ] && exit 1 || echo 'Coverage gate passed'Jest configuration for coverage thresholds:
{ "jest": { "coverageThreshold": { "global": { "lines": 80, "branches": 70, "functions": 85, "statements": 80 }, "./src/payments/": { "lines": 95 } } }}Practical Commands
## Run tests with coveragenpx jest --coveragepython -m pytest --cov=src --cov-report=term-missinggo test ./... -coverprofile=coverage.out && go tool cover -html=coverage.out ## Check coverage without running tests (from saved report)npx nyc report --reporter=text-summary ## Fail if below threshold (for CI use)npx jest --coverage --coverageThreshold='{"global":{"lines":80}}' ## Coverage diff: only check new code in PRnpx jest --coverage --changedSince=origin/mainTroubleshooting
| Symptom | Check | What to Look For |
|---|---|---|
| Coverage drops suddenly | git diff + coverage report |
Uncovered code in recent commits |
| Coverage gate passes but quality low | Branch vs line coverage | Switch to stricter branch coverage |
| Coverage gaming | Review what is being tested | Tests that execute code without assertions |
PLACEMENT PRO TIP**Tip:** Use per-directory coverage thresholds for critical code paths. Set `./src/payments/` to require 95% coverage and `./src/utils/` to require 75%. This ensures your most critical business logic is heavily tested without applying the strictest threshold everywhere.
REMEMBER THIS**Remember:** Coverage percentage measures code execution, not assertion quality. A test that calls every function but asserts nothing will show 100% coverage. Coverage gates prevent coverage from dropping -- they do not guarantee the tests are meaningful. Code review must verify that tests have real assertions.
COMMON MISTAKE / WARNING**Security:** Pay special attention to coverage of authentication, authorisation, and input validation code paths. A payment processing function with 60% branch coverage means 40% of its conditional logic is untested -- which in a payment context could mean untested paths through security-critical code.
COMMON MISTAKE / WARNING**Common Mistake:** Setting the coverage threshold too high too quickly. If coverage is currently at 65% and you immediately set the gate to 80%, every PR fails until someone writes 15% worth of tests. Start with the current coverage as the floor, then raise it by 2-3% per quarter until you reach your target.