iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Unit Tests

Unit tests in CI: fast feedback, deterministic, parallel, coverage. The shape that catches regressions without slowing PRs.

CI/CD — unit tests

EXAMPLE
# ===== Principles =====
# - Fast (< 5 minutes total)
# - Deterministic (no flaky tests; quarantine + fix)
# - Independent (any order works)
# - Self-validating (assertions, not console output)
# - Repeatable (no network, no shared state, no time/random without seed)

# ===== GitHub Actions =====
name: ci
on: [push, pull_request]
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
      - run: npm test -- --coverage --reporter=junit --outputFile=junit.xml
      - uses: actions/upload-artifact@v4
        if: always()
        with: { name: junit, path: junit.xml }
      - uses: codecov/codecov-action@v4
        with: { files: ./coverage/lcov.info }

# ===== Parallelism =====
# Vitest:
npm test -- --coverage --threads=4

# Jest:
npm test -- --coverage --maxWorkers=4

# Cross-job parallel via matrix:
strategy:
  matrix:
    shard: [1, 2, 3, 4]
steps:
  - run: npm test -- --shard=${{ matrix.shard }}/4

# ===== Speeding up =====
# - Cache deps (setup-node cache, pnpm install --frozen-lockfile)
# - Cache build artifacts (turbo, nx)
# - Run only affected tests on PR (turborepo, nx, just-run-affected)
# - Mark slow tests; run them in a separate suite

# ===== Coverage =====
# Pick a target (75-90% reasonable for app code), not 100% (diminishing returns).
# Lower for UI; higher for core business logic.

# Tool: Vitest / Jest / pytest-cov / dotnet test --collect:"XPlat Code Coverage"

# Fail PR if coverage drops:
- run: npm test -- --coverage
- name: Coverage threshold
  run: |
    PCT=$(jq '.total.lines.pct' coverage/coverage-summary.json)
    THRESH=80
    if (( $(echo "$PCT < $THRESH" | bc -l) )); then
      echo "Coverage $PCT < $THRESH"; exit 1
    fi

# ===== Junit XML output =====
# Most reporters can emit junit format; CI shows test results inline.
# GitHub Actions, GitLab, Azure DevOps all render junit results.

# ===== Test selection =====
# Run only changed files (great in monorepos):
- run: npx vitest related $(git diff --name-only origin/main)

# Or via turborepo:
- run: npx turbo run test --filter=...[origin/main]

# ===== Flaky tests =====
# Detect: re-run failed tests N times in CI; if pass after retry, mark flaky.
# Quarantine: temporarily skip / @flaky tag; ticket to fix.
# Address: time, randomness, network, shared state are the four sources.

# ===== Cache + parallel patterns =====
- uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      ~/.cache/turbo
    key: turbo-${{ github.sha }}
    restore-keys: |
      turbo-

# ===== When to break tests into stages =====
- Lint (5s)
- Typecheck (15s)
- Unit (1-5m)
- Integration (5-15m)
- E2E (10-30m; often nightly only)
# Order by speed; fail fast on the cheap ones.

# ===== Patterns to internalise =====
# - Fast, deterministic, parallel — the holy trinity
# - Coverage as a guardrail, not a vanity number
# - Junit reports for inline CI display
# - Affected-only test runs in monorepos

# ===== Pitfalls =====
# - Tests that hit network / real DB / real time -> flake
# - 'Pass on retry' culture hides real bugs
# - 100% coverage chase wastes time on trivial code
# - Cache invalidation bugs (key missing the lockfile hash)

Why it matters

Unit tests in CI: fast, deterministic, parallel, with coverage gates and inline junit reporting. Cache deps, shard or scope to affected files, and quarantine flakes rather than retrying. The discipline is what makes "merge when green" actually mean something.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
- run: npm run test:unit
# Or with coverage
- run: npm run test:unit -- --coverage
- uses: codecov/codecov-action@v5
Try it Yourself »

Discussion

Loading…