Skip to main content

Test Coverage Gate

A test coverage gate is a CI pipeline rule that fails the build if code test coverage drops below a defined threshold. Coverage gates enforce a minimum quality standard and prevent developers from shipping new code without corresponding tests.

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

◈ DIAGRAM
+------------------------------------------+
| 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:

TEXT
Line coverage: % of code lines executed by at least one test
Branch coverage: % of if/else branches tested (both true and false)
Function coverage: % of functions called by at least one test
Statement 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):

YAML
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:

JSON
{
"jest": {
"coverageThreshold": {
"global": {
"lines": 80,
"branches": 70,
"functions": 85,
"statements": 80
},
"./src/payments/": {
"lines": 95
}
}
}
}

Practical Commands

Bash
## Run tests with coverage
npx jest --coverage
python -m pytest --cov=src --cov-report=term-missing
go 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 PR
npx jest --coverage --changedSince=origin/main

Troubleshooting

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
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

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.

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

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.

Frequently Asked Questions

What coverage percentage do most teams actually enforce, and why not require 100%?

Common thresholds are 70-80% line or branch coverage, not 100%. Chasing 100% pushes developers to write low-value tests just to hit boilerplate, getters/setters, and error branches that rarely fail, which wastes time without meaningfully reducing defects. A coverage gate is meant to prevent large untested chunks of new logic from merging — it's a floor against regression risk, not a proxy for test quality.

What's a common way teams game or misuse a coverage gate?

Writing tests that execute code paths without asserting meaningful outcomes — calling a function just to bump coverage numbers while never checking its output, which passes the gate but catches nothing. Another pitfall: gating on total repo coverage instead of diff coverage on the changed lines, which lets a legacy codebase's high baseline coverage mask genuinely untested new code. Diff-coverage tools (e.g. Codecov's patch coverage) address this directly.