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:integrationWriting 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:27017PandaStack 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 productionA pipeline where deployment is gated on passing tests transforms git push into a confident, repeatable act of shipping.