Automated Testing in Git Workflows
Automated testing and Git workflows are inseparable in modern software development. This page covers every layer of the testing pyramid as it relates to your Git workflow: pre-push hooks for local assurance, CI gates on pull requests, branch protection rules, parallel test execution, coverage enforcement, and handling flaky tests.
The testing automation pyramid in Git
Pre-commit: fast checks only (lint, format). No tests here — too slow.
Pre-push hook: run unit tests locally before pushing to prevent broken PRs.
CI on pull request: full test suite — unit, integration, end-to-end.
Required status checks: block merging if any CI check fails.
Post-merge CI on main: final verification, deploy pipeline trigger.
Running tests on every commit via pre-push hook
Pre-commit hooks are too slow for a full test suite, but a pre-push hook can run unit tests before pushing — catching failures before they ever reach CI.
.husky/pre-push
#!/usr/bin/env sh echo "Running tests before push..." npm run test:unit # Exit 1 on test failure — push is aborted # Exit 0 on success — push proceeds
.git/hooks/pre-push (vanilla, no Husky)
#!/bin/sh npm run test:unit exit_code=$? if [ $exit_code -ne 0 ]; then echo "Tests failed. Push aborted." exit 1 fi
CI test gates on pull requests
Every pull request should trigger a CI workflow that runs the full test suite. Failures are visible inline on the PR before anyone reviews the code.
.github/workflows/test.yml
name: Tests
on:
pull_request:
branches: [main, develop]
push:
branches: [main]
jobs:
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run test:unit -- --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: testpass
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npm run test:integration
env:
DATABASE_URL: postgresql://postgres:testpass@localhost:5432/testdbBranch protection: require status checks before merging
Branch protection rules enforce that specific CI checks must pass before a pull request can be merged. This is the enforcement layer that makes CI checks mandatory.
GitHub: Settings → Branches → Add branch protection rule → main.
Enable "Require status checks to pass before merging".
Search for and select the specific checks (e.g. "Unit Tests", "Integration Tests").
Enable "Require branches to be up to date before merging" for stricter guarantees.
Enable "Restrict pushes that create matching branches" to prevent direct commits.
Test parallelisation in CI
As your test suite grows, splitting it across parallel jobs dramatically reduces CI time. The two main approaches are: manual splitting and automatic shard-based splitting.
Matrix-based test splitting
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4] # 4 parallel test runners
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- name: Run test shard ${{ matrix.shard }}/4
run: npx jest --shard=${{ matrix.shard }}/4Playwright E2E tests with sharding
jobs:
e2e:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1/4, 2/4, 3/4, 4/4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npx playwright install --with-deps chromium
- name: Run Playwright tests (shard ${{ matrix.shard }})
run: npx playwright test --shard=${{ matrix.shard }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-results-${{ strategy.job-index }}
path: test-results/Coverage gates
Enforce a minimum test coverage threshold so that coverage never drops below an acceptable level as new code is added.
Jest: coverage thresholds in jest.config.js
{
"jest": {
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}Vitest: coverage threshold in vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
provider: 'v8',
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
}
})Coverage threshold failure in CI
npm test -- --coverage # FAIL src/utils.test.ts # Jest: "global" coverage threshold for lines (80%) not met: 74% # Process exited with code 1 ← CI build fails, PR is blocked
Handling flaky tests
Flaky tests — tests that sometimes pass and sometimes fail with no code change — are one of the most frustrating problems in CI. Here are strategies to manage them.
Strategy | Description | When to use |
|---|---|---|
Retry in CI | Run failing tests up to N times automatically | Temporary workaround while investigating root cause |
Quarantine flag | Mark known flaky tests; run them separately and do not fail CI on them | Tests that are flaky for known external reasons (network, timing) |
Fix the root cause | Add proper async waits, use stable test IDs, mock external services | Always the real solution |
Flaky test reporting | Track flakiness rate over time via test result APIs | Large projects with CI observability requirements |
Retry flaky tests in GitHub Actions
- name: Run tests (with retry)
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm testJest retry via jest-circus
// jest.config.js
module.exports = {
runner: 'jest-circus/runner',
testRetries: 2, // retry failing tests up to 2 times
}Test result artifacts and reporting
Upload test results and annotate the PR
- name: Run tests
run: npm test -- --reporter=junit --outputFile=test-results.xml
continue-on-error: true
- name: Upload test results
uses: actions/upload-artifact@v4
with:
name: test-results
path: test-results.xml
- name: Annotate PR with test results
uses: dorny/test-reporter@v1
with:
name: Jest Tests
path: test-results.xml
reporter: jest-junitMatrix testing across environments
Scenario | Matrix strategy | Why |
|---|---|---|
Node.js library | node: [18, 20, 22] | Ensure compatibility across supported versions |
Cross-platform app | os: [ubuntu, windows, macos] | Catch OS-specific path and newline issues |
Database-backed app | db: [postgres, mysql, sqlite] | Test with all supported databases |
Browser tests | browser: [chromium, firefox, webkit] | Cross-browser compatibility |