Back to Blog
Tutorial7 min read2026-05-01

Automated Testing in CI/CD: A Practical Setup Guide

Set up a layered automated testing strategy in your CI/CD pipeline to catch bugs early, ship confidently, and keep deployment fast.

Why Automated Testing Is Non-Negotiable in CI/CD

Continuous deployment without automated tests is just automated chaos. Tests are the safety net that makes it possible to merge to main and deploy to production multiple times per day without lying awake at night. The goal is a test pyramid: many fast unit tests, fewer integration tests, and a handful of end-to-end tests.

The Test Pyramid in Practice

         /\
        /E2E\          ← Slow, expensive, test critical user journeys
       /------\
      / Integration\   ← Test service boundaries, APIs, databases
     /--------------\
    /   Unit Tests    \ ← Fast, isolated, test individual functions
   /------------------\

Run all layers in CI, but structure the pipeline so fast tests run first. A failing lint check should stop the build in 30 seconds, not after a 10-minute test suite.

Setting Up the CI Pipeline

# .github/workflows/test.yml
name: Automated Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    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 lint
      - run: npm test -- --coverage --ci
      - uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/

  integration-tests:
    name: Integration Tests
    needs: unit-tests
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: testuser
          POSTGRES_PASSWORD: testpass
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - name: Run migrations
        env:
          DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
        run: npm run db:migrate
      - name: Run integration tests
        env:
          DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
          NODE_ENV: test
        run: npm run test:integration

Writing Effective Unit Tests

Unit tests should be fast (under 1ms each), isolated (no network, no database), and focused on a single behaviour:

# Example: Jest unit test structure
describe('calculateInvoiceTotal', () => {
  it('applies discount to subtotal', () => {
    const result = calculateInvoiceTotal({
      subtotal: 100,
      discountPercent: 20
    });
    expect(result).toBe(80);
  });

  it('throws when subtotal is negative', () => {
    expect(() =>
      calculateInvoiceTotal({ subtotal: -10, discountPercent: 0 })
    ).toThrow('Subtotal cannot be negative');
  });
});

Target 80%+ line coverage for business-critical code. Coverage reports uploaded as artifacts are visible in the Actions UI.

Integration Tests with Real Services

Integration tests verify that your code works correctly with real databases, queues, and APIs. GitHub Actions services spin up Docker containers for you:

    services:
      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379
      mongodb:
        image: mongo:7
        ports:
          - 27017:27017

PandaStack supports PostgreSQL, MySQL, Redis, and MongoDB — you can mirror your production database type exactly in CI.

End-to-End Tests

E2E tests simulate a real user interacting with your application. Run them only on the main branch after deployment to staging:

  e2e-tests:
    name: E2E Tests
    needs: integration-tests
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - name: Install Playwright
        run: npx playwright install --with-deps chromium
      - name: Run E2E tests against staging
        env:
          BASE_URL: https://staging.your-app.pandastack.io
        run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/

Enforcing Coverage Thresholds

Prevent coverage regression by failing the build when coverage drops below your threshold:

# jest.config.js
module.exports = {
  coverageThreshold: {
    global: {
      branches: 70,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
};

Connecting Tests to PandaStack Deployments

Deploy to PandaStack only after all test stages pass:

  deploy:
    needs: [unit-tests, integration-tests, e2e-tests]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - run: npm install -g @pandastack/cli
      - run: panda login --token ${{ secrets.PANDA_TOKEN }}
      - run: panda deploy --branch main --env production

A pipeline where deployment is gated on passing tests transforms git push into a confident, repeatable act of shipping.

Ready to deploy?

Start free on PandaStack — no credit card required.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also