Stage 5 · Platform
Testing & Quality Gates
Integration Tests
Starting PostgreSQL, Redis, LocalStack, and Docker Compose services inside pipeline jobs.
Integration vs Unit Tests
Integration tests verify that components work together — your application with its database, message queue, or external API. They catch issues that unit tests miss: schema mismatches, connection pooling bugs, serialization errors, and contract violations.
Integration tests are slower and more fragile than unit tests. They require external services, take longer to set up, and can fail due to infrastructure issues. Run them after unit tests and before deployment to catch issues that unit tests cannot.
GitHub Actions Services
jobs:
integration-test:
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
redis:
image: redis:7
ports:
- 6379:6379
localstack:
image: localstack/localstack:3
ports:
- 4566:4566
env:
SERVICES: s3,dynamodb,sqs
DEFAULT_REGION: us-east-1
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
- run: npm ci
- name: Run integration tests
run: npm test -- --integration
env:
DATABASE_URL: postgres://testuser:testpass@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
AWS_ENDPOINT: http://localhost:4566
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: testService containers run alongside the job. The health check options ensure Postgres is ready before tests run. Tests connect to services via localhost using the mapped ports. LocalStack mocks AWS services for testing.
Docker Compose in CI
jobs:
integration-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Start services
run: docker compose -f docker-compose.test.yml up -d --wait
- name: Run database migrations
run: docker compose -f docker-compose.test.yml exec app npm run migrate
- name: Seed test data
run: docker compose -f docker-compose.test.yml exec app npm run seed
- name: Run integration tests
run: docker compose -f docker-compose.test.yml exec app npm test -- --integration
- name: Collect logs on failure
if: failure()
run: docker compose -f docker-compose.test.yml logs > integration-logs.txt
- name: Upload logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: integration-logs
path: integration-logs.txt
- name: Cleanup
if: always()
run: docker compose -f docker-compose.test.yml down -vdocker-compose.test.yml defines all services needed for integration tests. The --wait flag waits for health checks to pass. if: failure() collects logs for debugging. if: always() ensures cleanup runs even when tests fail.
services:
app:
build:
context: .
dockerfile: Dockerfile.test
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
DATABASE_URL: postgres://testuser:testpass@postgres:5432/testdb
REDIS_URL: redis://redis:6379
postgres:
image: postgres:16
environment:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
POSTGRES_DB: testdb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5The test Dockerfile includes test dependencies and test configuration. The depends_on with condition: service_healthy ensures the app does not start until databases are ready. Health checks prevent premature connection attempts.
Database Integration Tests
Database tests verify that your application correctly interacts with the database layer. They test migrations, queries, transactions, and schema changes. Use a dedicated test database that is recreated for each test run.
test:integration:
stage: test
image: node:20
services:
- postgres:16
- redis:7
variables:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb_test
DATABASE_URL: "postgres://test:test@postgres:5432/testdb_test"
REDIS_URL: "redis://redis:6379"
script:
- npm ci
- npm run db:migrate
- npm run db:seed
- npm test -- --integration
after_script:
- npm run db:rollback
artifacts:
when: failure
reports:
junit: test-results/junit.xml
paths:
- test-results/GitLab CI services are accessible by their image name (postgres, redis). The after_script runs cleanup even when the test fails. Artifacts include test results only on failure to save storage.
External Service Mocking
Not every external service can run in CI. For services you do not control (payment processors, email providers, third-party APIs), use mock servers or service virtualization.
- WireMock — Java-based mock server for HTTP APIs. Record and replay real API responses.
- MSW (Mock Service Worker) — JavaScript interception layer for browser and Node.js API mocking.
- LocalStack — AWS service emulator for S3, DynamoDB, Lambda, and other AWS services.
- Testcontainers — Spin up Docker containers for any service (databases, message brokers, custom servers).
Test Data Management
Each test run should use an isolated database that is created before tests and destroyed after. Never share a test database between concurrent pipeline runs. Use per-test transactions that roll back after each test for maximum isolation.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.