Testing in CI — Unit, Integration & E2E
IntermediateEffective CI testing runs fast unit tests first, then slower integration tests with real dependencies (Docker), then E2E tests. Parallelisation, test splitting, and smart caching keep pipelines fast.
Overview
The test pyramid defines the optimal balance: many fast unit tests (milliseconds each), fewer integration tests (seconds each, need real DBs), even fewer E2E tests (minutes each, full browser/API flows). In CI, the ordering matters: run the cheapest tests first to fail fast. Docker Compose services in CI spin up real dependencies (PostgreSQL, Redis) for integration tests, matching production more closely than mocks. Parallelisation (splitting test files across runners) and intelligent caching (storing test results) keep pipelines under 10 minutes even for large codebases.
Test Pyramid in CI
Structure your pipeline stages to match the test pyramid: fast at the bottom, slow at the top. Fail the pipeline at the cheapest stage possible.
# .github/workflows/test.yml
name: Test Suite
on: [push, pull_request]
jobs:
# Stage 1: Fast checks (< 1 minute) — fail fast
lint-and-typecheck:
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 lint # ESLint, Prettier
- run: npm run typecheck # TypeScript tsc --noEmit
# Stage 2: Unit tests (< 3 minutes) — run if lint passes
unit-tests:
needs: lint-and-typecheck
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 test -- --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
# Stage 3: Integration tests (< 5 minutes) — real DB/Redis
integration-tests:
needs: unit-tests
runs-on: ubuntu-latest
services: # GitHub-managed Docker services
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports: ['6379:6379']
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: postgres://postgres:test@localhost:5432/testdb
REDIS_URL: redis://localhost:6379Parallel Test Splitting
Large test suites can be split across multiple runner instances using matrix strategy or test splitting tools. This turns a 15-minute suite into 3 minutes across 5 parallel runners.
jobs:
# Split unit tests across 4 parallel shards
unit-tests:
strategy:
matrix:
shard: [1, 2, 3, 4]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
# Jest built-in shard support
- run: npx jest --shard=${{ matrix.shard }}/4 --coverage
- uses: actions/upload-artifact@v4
with:
name: coverage-shard-${{ matrix.shard }}
path: coverage/
# Merge coverage reports from all shards
coverage-report:
needs: unit-tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
pattern: coverage-shard-*
merge-multiple: true
path: coverage/
- run: npx nyc merge coverage coverage/merged.json
- run: npx nyc report --reporter=lcov
# Playwright E2E split across browsers + shards:
e2e:
strategy:
matrix:
browser: [chromium, firefox]
shard: [1, 2]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci && npx playwright install --with-deps ${{ matrix.browser }}
- run: npx playwright test --shard=${{ matrix.shard }}/2 --project=${{ matrix.browser }}Test Results, PR Comments & Branch Protection
Publishing test results as PR comments and enforcing coverage thresholds as branch protection rules creates a quality gate that prevents regressions.
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
# Jest with JUnit reporter for structured results
- run: npm test -- --reporters=default --reporters=jest-junit
env:
JEST_JUNIT_OUTPUT_FILE: junit.xml
continue-on-error: true # don't fail yet — post results first
id: tests
# Post test results as PR comment
- name: Test Report
uses: dorny/test-reporter@v1
if: always()
with:
name: Jest Test Results
path: junit.xml
reporter: jest-junit
# Enforce coverage threshold
- name: Check coverage
run: |
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
echo "Coverage: $COVERAGE%"
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "Coverage $COVERAGE% is below 80% threshold"
exit 1
fi
# Fail job if tests failed
- name: Fail if tests failed
if: steps.tests.outcome == 'failure'
run: exit 1
# Branch Protection Rules (GitHub Settings):
# Require status checks to pass before merging:
# - lint-and-typecheck
# - unit-tests
# - integration-tests
# Require branches to be up to date
# Require pull request reviews: 1 approver
# This enforces quality gates at the repo level, not just in workflowsKey Points to Remember
- 1Test pyramid: unit tests first (fast, many), integration tests second, E2E last (slow, few).
- 2GitHub services: block in jobs spins up Docker containers (Postgres, Redis) as sidecar services for integration tests.
- 3Jest sharding (--shard=N/TOTAL) splits test files across parallel runners — linear speedup.
- 4dorny/test-reporter posts JUnit results as PR comments — no external service needed.
- 5Branch protection rules enforce required status checks — cannot merge without passing CI.
- 6continue-on-error: true + explicit failure step lets you collect test artifacts before failing the job.
Interview Questions
Sign in to ask AriaHow do you run tests against a real database in GitHub Actions?
How do you speed up a slow test suite in CI?
Ask Aria about Testing in CI — Unit, Integration & E2E
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.