Chương 15: CI/CD và Test Automation
Tóm Tắt
Hướng dẫn tích hợp test automation vào CI/CD pipeline, bao gồm GitHub Actions, GitLab CI, Docker, và best practices cho continuous testing.
Tổng Quan CI/CD Testing
Mục Tiêu
- Tự động chạy tests trên mỗi commit
- Fail build nếu tests fail
- Generate và publish test reports
- Parallel test execution
- Test trên multiple environments
Pipeline Flow
Code Push → Build → Unit Tests → API Tests → E2E Tests → Deploy
↓ ↓ ↓ ↓
Fail? Fail? Fail? Fail?
↓ ↓ ↓ ↓
Stop Stop Stop Stop
GitHub Actions
Basic Workflow
# .github/workflows/test.yml
name: Test Suite
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
api-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
- name: Run API tests
run: npm run test:api
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
JWT_SECRET: test-secret
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run E2E tests
run: npm run test:e2e
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- name: Upload videos
if: failure()
uses: actions/upload-artifact@v3
with:
name: test-videos
path: test-results/
Advanced Workflow với Matrix
# .github/workflows/test-matrix.yml
name: Test Matrix
on: [push, pull_request]
jobs:
test:
runs-on: $
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [16, 18, 20]
browser: [chromium, firefox, webkit]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js $
uses: actions/setup-node@v3
with:
node-version: $
- name: Install dependencies
run: npm ci
- name: Run tests on $
run: npx playwright test --project=$
Parallel Test Execution
# .github/workflows/parallel-tests.yml
name: Parallel Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run tests (shard $/4)
run: npx playwright test --shard=$/4
- name: Upload blob report
uses: actions/upload-artifact@v3
with:
name: blob-report-$
path: blob-report
retention-days: 1
merge-reports:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Download blob reports
uses: actions/download-artifact@v3
with:
path: all-blob-reports
- name: Merge reports
run: npx playwright merge-reports --reporter html ./all-blob-reports
- name: Upload HTML report
uses: actions/upload-artifact@v3
with:
name: html-report
path: playwright-report
Scheduled Tests
# .github/workflows/scheduled-tests.yml
name: Scheduled Tests
on:
schedule:
# Run every day at 2 AM UTC
- cron: '0 2 * * *'
workflow_dispatch: # Manual trigger
jobs:
smoke-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run smoke tests
run: npm run test:smoke
env:
BASE_URL: https://production.example.com
- name: Notify on failure
if: failure()
uses: 8398a7/action-slack@v3
with:
status: $
text: 'Smoke tests failed!'
webhook_url: $
GitLab CI
Basic Pipeline
# .gitlab-ci.yml
stages:
- build
- test
- report
- deploy
variables:
NODE_VERSION: "18"
POSTGRES_DB: test_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
build:
stage: build
image: node:${NODE_VERSION}
script:
- npm ci
cache:
paths:
- node_modules/
artifacts:
paths:
- node_modules/
expire_in: 1 hour
unit-tests:
stage: test
image: node:${NODE_VERSION}
dependencies:
- build
script:
- npm run test:unit
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
api-tests:
stage: test
image: node:${NODE_VERSION}
services:
- postgres:15
dependencies:
- build
variables:
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/test_db
script:
- npx prisma migrate deploy
- npm run test:api
artifacts:
reports:
junit: test-results/junit.xml
e2e-tests:
stage: test
image: mcr.microsoft.com/playwright:v1.40.0-focal
dependencies:
- build
script:
- npm run test:e2e
artifacts:
when: always
paths:
- playwright-report/
- test-results/
expire_in: 30 days
generate-report:
stage: report
image: node:${NODE_VERSION}
dependencies:
- unit-tests
- api-tests
- e2e-tests
script:
- npm run test:report
artifacts:
paths:
- allure-report/
expire_in: 30 days
only:
- main
- develop
Parallel Execution
# .gitlab-ci.yml
e2e-tests:
stage: test
image: mcr.microsoft.com/playwright:v1.40.0-focal
parallel: 4
script:
- npx playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
artifacts:
when: always
paths:
- test-results/
Docker Support
Dockerfile for Testing
# Dockerfile.test
FROM node:18-alpine
# Install Playwright dependencies
RUN apk add --no-cache \
chromium \
firefox \
webkit \
ffmpeg
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY prisma ./prisma/
# Install dependencies
RUN npm ci
# Copy source code
COPY . .
# Generate Prisma client
RUN npx prisma generate
# Set environment variables
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
ENV NODE_ENV=test
# Run tests
CMD ["npm", "test"]
Docker Compose for Testing
# docker-compose.test.yml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: test_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
test-runner:
build:
context: .
dockerfile: Dockerfile.test
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/test_db
REDIS_URL: redis://redis:6379
JWT_SECRET: test-secret
volumes:
- ./test-results:/app/test-results
- ./coverage:/app/coverage
command: npm test
Run Tests in Docker
# Build and run tests
docker-compose -f docker-compose.test.yml up --build --abort-on-container-exit
# Run specific test suite
docker-compose -f docker-compose.test.yml run test-runner npm run test:e2e
# Clean up
docker-compose -f docker-compose.test.yml down -v
Test Reports
Allure Reports
// tests/config/allure.setup.ts
import { allure } from 'allure-playwright';
export function setupAllure() {
// Add environment info
allure.writeEnvironmentInfo({
'Node Version': process.version,
'OS': process.platform,
'Browser': 'Chromium',
'Test Environment': process.env.TEST_ENV || 'local'
});
// Add categories
allure.writeCategoriesDefinitions([
{
name: 'Failed tests',
matchedStatuses: ['failed']
},
{
name: 'Broken tests',
matchedStatuses: ['broken']
},
{
name: 'Flaky tests',
messageRegex: '.*flaky.*'
}
]);
}
Jest HTML Reporter
// jest.config.ts
export default {
reporters: [
'default',
[
'jest-html-reporter',
{
pageTitle: 'Test Report',
outputPath: 'test-results/index.html',
includeFailureMsg: true,
includeConsoleLog: true,
sort: 'status'
}
]
]
};
Playwright HTML Reporter
// playwright.config.ts
export default {
reporter: [
['html', {
outputFolder: 'playwright-report',
open: 'never'
}],
['json', {
outputFile: 'test-results/results.json'
}],
['junit', {
outputFile: 'test-results/junit.xml'
}]
]
};
Notifications
Slack Notifications
# .github/workflows/test-with-notifications.yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run tests
run: npm test
- name: Notify Slack on success
if: success()
uses: 8398a7/action-slack@v3
with:
status: custom
custom_payload: |
{
text: "✅ Tests passed!",
attachments: [{
color: 'good',
text: `Branch: ${process.env.GITHUB_REF}\nCommit: ${process.env.GITHUB_SHA}`
}]
}
webhook_url: $
- name: Notify Slack on failure
if: failure()
uses: 8398a7/action-slack@v3
with:
status: custom
custom_payload: |
{
text: "❌ Tests failed!",
attachments: [{
color: 'danger',
text: `Branch: ${process.env.GITHUB_REF}\nCommit: ${process.env.GITHUB_SHA}\nView: ${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
}]
}
webhook_url: $
Email Notifications
# .github/workflows/test-with-email.yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run tests
run: npm test
- name: Send email on failure
if: failure()
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.gmail.com
server_port: 465
username: $
password: $
subject: Test Failed - $
body: |
Tests failed in $
Branch: $
Commit: $
Author: $
View details: $/$/actions/runs/$
to: team@example.com
from: ci@example.com
Best Practices
1. Fast Feedback
# Run fast tests first
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: npm run lint # 30 seconds
unit-tests:
needs: lint
runs-on: ubuntu-latest
steps:
- run: npm run test:unit # 2 minutes
api-tests:
needs: unit-tests
runs-on: ubuntu-latest
steps:
- run: npm run test:api # 5 minutes
e2e-tests:
needs: api-tests
runs-on: ubuntu-latest
steps:
- run: npm run test:e2e # 10 minutes
2. Caching
# Cache dependencies
- name: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
key: $-node-$
restore-keys: |
$-node-
# Cache Playwright browsers
- name: Cache Playwright browsers
uses: actions/cache@v3
with:
path: ~/.cache/ms-playwright
key: $-playwright-$
3. Retry Flaky Tests
// playwright.config.ts
export default {
retries: process.env.CI ? 2 : 0,
use: {
// Retry failed assertions
actionTimeout: 10000,
navigationTimeout: 30000
}
};
4. Test Isolation
// tests/setup/global-setup.ts
export default async function globalSetup() {
// Create fresh test database
await exec('createdb test_db');
// Run migrations
await exec('npx prisma migrate deploy');
}
// tests/setup/global-teardown.ts
export default async function globalTeardown() {
// Drop test database
await exec('dropdb test_db');
}
5. Environment Variables
# .github/workflows/test.yml
env:
NODE_ENV: test
DATABASE_URL: $
JWT_SECRET: $
API_KEY: $
6. Artifacts
# Save test artifacts
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: test-results
path: |
test-results/
playwright-report/
coverage/
retention-days: 30
Monitoring và Analytics
Test Metrics
// scripts/analyze-tests.ts
import fs from 'fs';
interface TestResult {
name: string;
duration: number;
status: 'passed' | 'failed' | 'skipped';
}
function analyzeTests() {
const results: TestResult[] = JSON.parse(
fs.readFileSync('test-results/results.json', 'utf-8')
);
const metrics = {
total: results.length,
passed: results.filter(t => t.status === 'passed').length,
failed: results.filter(t => t.status === 'failed').length,
skipped: results.filter(t => t.status === 'skipped').length,
avgDuration: results.reduce((sum, t) => sum + t.duration, 0) / results.length,
slowestTests: results
.sort((a, b) => b.duration - a.duration)
.slice(0, 10)
};
console.log('Test Metrics:', metrics);
// Send to monitoring service
// await sendToDatadog(metrics);
}
analyzeTests();
Kết Luận
CI/CD testing setup hoàn chỉnh với:
- ✅ GitHub Actions / GitLab CI
- ✅ Docker support
- ✅ Parallel execution
- ✅ Test reports
- ✅ Notifications
- ✅ Best practices
Chương tiếp theo: Quản Lý Test Data và Fixtures
Bài viết được viết bằng AI 🚀