GitAutomated Testing on Push

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

Bash
#!/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)

Bash
#!/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

YAML
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/testdb
Branch 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.

Status check names
The status check name must exactly match the `name:` field of the job in your workflow YAML. If you rename a job, update the branch protection rule too.
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

YAML
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 }}/4

Playwright E2E tests with sharding

YAML
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

JSON
{
  "jest": {
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    }
  }
}

Vitest: coverage threshold in vitest.config.ts

YAML
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

Bash
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

YAML
- name: Run tests (with retry)
  uses: nick-fields/retry@v3
  with:
    timeout_minutes: 10
    max_attempts: 3
    command: npm test

Jest retry via jest-circus

JSON
// jest.config.js
module.exports = {
  runner: 'jest-circus/runner',
  testRetries: 2,   // retry failing tests up to 2 times
}
Warning
Retrying flaky tests is a band-aid. Tests that rely on timing, external services, or global state will continue to flake. Invest time in fixing them properly — use `vi.useFakeTimers()`, mock network calls with MSW, and isolate state between tests.
Test result artifacts and reporting

Upload test results and annotate the PR

YAML
- 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-junit
Matrix 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

Tip
On pull requests, run a minimal matrix (e.g. just Node 20 on Ubuntu) to keep PR feedback fast. Run the full matrix only on merge to main to conserve CI minutes.