CI/CD & DevOps
Automated Testing in Pipelines
Design a CI pipeline that runs the right tests at the right time — fast feedback without compromising coverage.
Test Ordering in CI
The goal of test ordering is simple: fail fast. Discover problems as early as possible in the pipeline to save time.
The recommended order from fastest to slowest:
- Lint (seconds) — catches obvious code issues
- Type check (seconds) — catches type errors without running code
- Unit tests (seconds to minutes) — fast, isolated tests
- Integration tests (minutes) — tests with real databases, services
- Build (minutes) — verify the production build works
- E2E tests (minutes to tens of minutes) — slow, run last
If linting fails in 10 seconds, there is no reason to wait 10 minutes for E2E tests to run.
Parallelization
Run independent checks simultaneously:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci && npm run lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci && npm run typecheck
test:
needs: [lint, typecheck] # Wait for quality checks first
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci && npm test -- --coverageLint and typecheck run in parallel. Tests only run if both pass.
Code Coverage in CI
Track coverage and fail if it drops below a threshold:
- name: Run tests with coverage
run: npm test -- --coverage --coverageThreshold='{"global":{"lines":80}}'
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}Configure thresholds in your test runner config:
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
thresholds: {
lines: 80,
branches: 70,
functions: 80,
},
},
},
});Security Scanning in CI
Add automated security checks to every pipeline:
- name: Security audit
run: npm audit --audit-level=high
- name: CodeQL analysis
uses: github/codeql-action/analyze@v3
with:
languages: javascript, typescriptnpm audit fails the pipeline if any high or critical severity vulnerabilities are found in dependencies.
Database Testing with Service Containers
Run tests against a real database in CI:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
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 db:migrate
env:
DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
- run: npm test
env:
DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdbThe database starts as a Docker service, migrations run, tests execute against the real database.
Key Takeaways
- Order tests from fastest to slowest: lint → typecheck → unit → integration → build → E2E
- Run independent jobs (lint and typecheck) in parallel to minimize total pipeline time
- Set coverage thresholds that fail the pipeline — this enforces test investment over time
- Security audits belong in CI — catch vulnerable dependencies before they reach production
- Use service containers for database tests in CI — real database, real queries, real confidence
Example
# Complete CI pipeline with parallel jobs and coverage
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci && npm run lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci && npm run typecheck
test:
needs: [lint, typecheck]
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env: { POSTGRES_DB: test, POSTGRES_USER: test, POSTGRES_PASSWORD: test }
options: --health-cmd pg_isready --health-interval 10s --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 test -- --coverage
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test