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:

  1. Lint (seconds) — catches obvious code issues
  2. Type check (seconds) — catches type errors without running code
  3. Unit tests (seconds to minutes) — fast, isolated tests
  4. Integration tests (minutes) — tests with real databases, services
  5. Build (minutes) — verify the production build works
  6. 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:

yaml
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 -- --coverage

Lint 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:

yaml
- 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:

typescript
// 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:

yaml
- name: Security audit
  run: npm audit --audit-level=high

- name: CodeQL analysis
  uses: github/codeql-action/analyze@v3
  with:
    languages: javascript, typescript

npm 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:

yaml
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/testdb

The 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

yaml
# 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
Try it yourself — YAML

Docker, AWS, Vercel, Netlify, GitHub, GitHub Actions are trademarks of Docker, Inc., Amazon.com, Inc., Vercel, Inc., Netlify, Inc., Microsoft Corporation. DevForge Academy is not affiliated with, endorsed by, or sponsored by these companies. Referenced for educational purposes only. See full disclaimers