Writing tests is only half the story. You need to measure how much of your code is actually tested, enforce coverage standards, and automate test execution so that no untested code slips into production.
Understanding Code Coverage
Code coverage measures which lines, branches, functions, and statements your tests execute. Jest has built-in coverage support — no extra tools needed:
npx jest --coverageThis generates a coverage summary in the terminal:
--------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
--------------------|---------|----------|---------|---------|
All files | 87.5 | 75.0 | 90.0 | 88.2 |
formatPrice.ts | 100.0 | 100.0 | 100.0 | 100.0 |
userService.ts | 80.0 | 60.0 | 85.7 | 81.5 |
utils.ts | 72.0 | 55.0 | 66.7 | 73.3 |
--------------------|---------|----------|---------|---------|The Four Metrics
- Statements: Percentage of executable statements that ran.
- Branches: Percentage of
if/else,switch, and ternary paths taken. - Functions: Percentage of functions that were called at least once.
- Lines: Percentage of lines that executed.
Branch coverage is typically the hardest to achieve and the most valuable. It catches untested error paths and edge cases.
Configuring Coverage Thresholds
Set minimum coverage requirements in jest.config.js:
module.exports = {
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/index.ts',
],
coverageThreshold: {
global: {
branches: 80,
functions: 85,
lines: 85,
statements: 85,
},
'./src/utils/': {
branches: 95,
functions: 100,
lines: 95,
statements: 95,
},
},
};When coverage drops below these thresholds, jest --coverage exits with a non-zero code. The collectCoverageFrom pattern controls which files are included — exclude type definitions and barrel files that do not contain logic.
You can set stricter thresholds for critical paths (like utility functions) and relaxed ones for the overall project.
Reading the HTML Report
Jest generates a detailed HTML report in the coverage/lcov-report/ directory:
npx jest --coverage
open coverage/lcov-report/index.htmlThe HTML report highlights every file with color-coded lines:
- Green: Covered by tests.
- Red: Not covered — no test executed this line.
- Yellow: Partially covered — some branches taken, others not.
Click into a file to see exactly which if branches are untested. This is the fastest way to identify gaps.
Integrating Jest in GitHub Actions
Automate test execution on every push and pull request:
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests with coverage
run: npx jest --coverage --ci
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/lcov-report/
retention-days: 7The --ci flag makes Jest run in continuous integration mode — it fails if no tests are found (instead of passing silently) and disables the watch mode interactive prompts.
Pre-Commit Hooks with Husky
Catch test failures before they even reach CI by running tests on every commit:
npm install -D husky lint-staged
npx husky initConfigure lint-staged in package.json:
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"jest --bail --findRelatedTests"
]
}
}Add the hook:
echo "npx lint-staged" > .husky/pre-commitThe --findRelatedTests flag tells Jest to only run tests that are affected by the staged files. The --bail flag stops at the first failure for fast feedback. This means a commit that breaks a test will be rejected locally — no waiting for CI.
Coverage Is Not Quality
High coverage does not guarantee good tests. A test that calls a function without asserting the result adds coverage but catches nothing. Focus on:
- Meaningful assertions: Every test should verify specific behavior.
- Edge cases: Zero, null, empty strings, boundary values, error paths.
- Branch coverage: Untested
if/elsebranches are where bugs hide. - Mutation testing: Tools like Stryker modify your code and check if tests catch the change. This measures test quality, not just test quantity.
Use coverage as a tool to find gaps, not as a target to hit blindly.
Key Takeaways
- Run
jest --coverageto generate coverage reports with line-by-line detail. - Set
coverageThresholdin Jest config to enforce minimums in CI. - Use the HTML report to identify uncovered branches and lines.
- Automate tests in GitHub Actions with
--ciand--coverageflags. - Add pre-commit hooks with Husky and
--findRelatedTestsfor instant local feedback. - Treat coverage as a diagnostic tool, not a metric to game.