10 KiB
10 KiB
Backend Testing Strategies
Comprehensive testing approaches, frameworks, and quality assurance practices (2025).
Test Pyramid (70-20-10 Rule)
/\
/E2E\ 10% - End-to-End Tests
/------\
/Integr.\ 20% - Integration Tests
/----------\
/ Unit \ 70% - Unit Tests
/--------------\
Rationale:
- Unit tests: Fast, cheap, isolate bugs quickly
- Integration tests: Verify component interactions
- E2E tests: Expensive, slow, but validate real user flows
Unit Testing
Frameworks by Language
TypeScript/JavaScript:
- Vitest - 50% faster than Jest in CI/CD, ESM native
- Jest - Mature, large ecosystem, snapshot testing
Python:
- Pytest - Industry standard, fixtures, parametrization
- Unittest - Built-in, standard library
Go:
- testing - Built-in, table-driven tests
- testify - Assertions and mocking
Best Practices
// Good: Test single responsibility
describe('UserService', () => {
describe('createUser', () => {
it('should create user with valid data', async () => {
const userData = { email: 'test@example.com', name: 'Test' };
const user = await userService.createUser(userData);
expect(user).toMatchObject(userData);
expect(user.id).toBeDefined();
});
it('should throw error with duplicate email', async () => {
const userData = { email: 'existing@example.com', name: 'Test' };
await expect(userService.createUser(userData))
.rejects.toThrow('Email already exists');
});
it('should hash password before storing', async () => {
const userData = { email: 'test@example.com', password: 'plain123' };
const user = await userService.createUser(userData);
expect(user.password).not.toBe('plain123');
expect(user.password).toMatch(/^\$argon2id\$/);
});
});
});
Mocking
// Mock external dependencies
jest.mock('./emailService');
it('should send welcome email after user creation', async () => {
const emailService = require('./emailService');
emailService.sendWelcomeEmail = jest.fn();
await userService.createUser({ email: 'test@example.com' });
expect(emailService.sendWelcomeEmail).toHaveBeenCalledWith('test@example.com');
});
Integration Testing
API Integration Tests
import request from 'supertest';
import { app } from '../app';
describe('POST /api/users', () => {
beforeAll(async () => {
await db.connect(); // Real database connection (test DB)
});
afterAll(async () => {
await db.disconnect();
});
beforeEach(async () => {
await db.users.deleteMany({}); // Clean state
});
it('should create user and return 201', async () => {
const response = await request(app)
.post('/api/users')
.send({ email: 'test@example.com', name: 'Test User' })
.expect(201);
expect(response.body).toMatchObject({
email: 'test@example.com',
name: 'Test User',
});
// Verify database persistence
const user = await db.users.findOne({ email: 'test@example.com' });
expect(user).toBeDefined();
});
it('should return 400 for invalid email', async () => {
await request(app)
.post('/api/users')
.send({ email: 'invalid-email', name: 'Test' })
.expect(400)
.expect((res) => {
expect(res.body.error).toBe('Invalid email format');
});
});
});
Database Testing with TestContainers
import { GenericContainer } from 'testcontainers';
let container;
let db;
beforeAll(async () => {
// Spin up real PostgreSQL in Docker
container = await new GenericContainer('postgres:15')
.withEnvironment({ POSTGRES_PASSWORD: 'test' })
.withExposedPorts(5432)
.start();
const port = container.getMappedPort(5432);
db = await createConnection({
host: 'localhost',
port,
database: 'test',
password: 'test',
});
}, 60000);
afterAll(async () => {
await container.stop();
});
Contract Testing (Microservices)
Pact (Consumer-Driven Contracts)
// Consumer test
import { Pact } from '@pact-foundation/pact';
const provider = new Pact({
consumer: 'UserService',
provider: 'AuthService',
});
describe('Auth Service Contract', () => {
beforeAll(() => provider.setup());
afterEach(() => provider.verify());
afterAll(() => provider.finalize());
it('should validate user token', async () => {
await provider.addInteraction({
state: 'user token exists',
uponReceiving: 'a request to validate token',
withRequest: {
method: 'POST',
path: '/auth/validate',
headers: { 'Content-Type': 'application/json' },
body: { token: 'valid-token-123' },
},
willRespondWith: {
status: 200,
body: { valid: true, userId: '123' },
},
});
const response = await authClient.validateToken('valid-token-123');
expect(response.valid).toBe(true);
});
});
Load Testing
Tools Comparison
k6 (Modern, Developer-Friendly)
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 }, // Ramp up to 100 users
{ duration: '5m', target: 100 }, // Stay at 100 users
{ duration: '2m', target: 0 }, // Ramp down to 0 users
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% requests under 500ms
},
};
export default function () {
const res = http.get('https://api.example.com/users');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
Gatling (JVM-based, Advanced Scenarios) JMeter (GUI-based, Traditional)
Performance Thresholds
- Response time: p95 < 500ms, p99 < 1s
- Throughput: 1000+ req/sec (target based on SLA)
- Error rate: < 1%
- Concurrent users: Test at 2x expected peak
E2E Testing
Playwright (Modern, Multi-Browser)
import { test, expect } from '@playwright/test';
test('user can register and login', async ({ page }) => {
// Navigate to registration page
await page.goto('https://app.example.com/register');
// Fill registration form
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'SecurePass123!');
await page.click('button[type="submit"]');
// Verify redirect to dashboard
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Welcome');
// Verify API call was made
const response = await page.waitForResponse('/api/users');
expect(response.status()).toBe(201);
});
Database Migration Testing
Critical: 83% migrations fail without proper testing
describe('Database Migrations', () => {
it('should migrate from v1 to v2 without data loss', async () => {
// Insert test data in v1 schema
await db.query(`
INSERT INTO users (id, email, name)
VALUES (1, 'test@example.com', 'Test User')
`);
// Run migration
await runMigration('v2-add-created-at.sql');
// Verify v2 schema
const result = await db.query('SELECT * FROM users WHERE id = 1');
expect(result.rows[0]).toMatchObject({
id: 1,
email: 'test@example.com',
name: 'Test User',
created_at: expect.any(Date),
});
});
it('should rollback migration successfully', async () => {
await runMigration('v2-add-created-at.sql');
await rollbackMigration('v2-add-created-at.sql');
// Verify v1 schema restored
const columns = await db.query(`
SELECT column_name FROM information_schema.columns
WHERE table_name = 'users'
`);
expect(columns.rows.map(r => r.column_name)).not.toContain('created_at');
});
});
Security Testing
SAST (Static Application Security Testing)
# SonarQube for code quality + security
sonar-scanner \
-Dsonar.projectKey=my-backend \
-Dsonar.sources=src \
-Dsonar.host.url=http://localhost:9000
# Semgrep for security patterns
semgrep --config auto src/
DAST (Dynamic Application Security Testing)
# OWASP ZAP for runtime security scanning
docker run -t owasp/zap2docker-stable zap-baseline.py \
-t https://api.example.com \
-r zap-report.html
Dependency Scanning (SCA)
# npm audit for Node.js
npm audit fix
# Snyk for multi-language
snyk test
snyk monitor # Continuous monitoring
Code Coverage
Target Metrics (SonarQube Standards)
- Overall coverage: 80%+
- Critical paths: 100% (authentication, payment, data integrity)
- New code: 90%+
Implementation
# Vitest with coverage
vitest run --coverage
# Jest with coverage
jest --coverage --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80}}'
CI/CD Testing Pipeline
# GitHub Actions example
name: Test Pipeline
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Unit Tests
run: npm run test:unit
- name: Integration Tests
run: npm run test:integration
- name: E2E Tests
run: npm run test:e2e
- name: Load Tests
run: k6 run load-test.js
- name: Security Scan
run: npm audit && snyk test
- name: Coverage Report
run: npm run test:coverage
- name: Upload to Codecov
uses: codecov/codecov-action@v3
Testing Best Practices
- Arrange-Act-Assert (AAA) Pattern
- One assertion per test (when practical)
- Descriptive test names -
should throw error when email is invalid - Test edge cases - Empty inputs, boundary values, null/undefined
- Clean test data - Reset database state between tests
- Fast tests - Unit tests < 10ms, Integration < 100ms
- Deterministic - No flaky tests, avoid sleep(), use waitFor()
- Independent - Tests don't depend on execution order
Testing Checklist
- Unit tests cover 70% of codebase
- Integration tests for all API endpoints
- Contract tests for microservices
- Load tests configured (k6/Gatling)
- E2E tests for critical user flows
- Database migration tests
- Security scanning in CI/CD (SAST, DAST, SCA)
- Code coverage reports automated
- Tests run on every PR
- Flaky tests eliminated
Resources
- Vitest: https://vitest.dev/
- Playwright: https://playwright.dev/
- k6: https://k6.io/docs/
- Pact: https://docs.pact.io/
- TestContainers: https://testcontainers.com/