430 lines
10 KiB
Markdown
430 lines
10 KiB
Markdown
# 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
// 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)
|
|
```javascript
|
|
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)
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```bash
|
|
# 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)
|
|
|
|
```bash
|
|
# 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)
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# Vitest with coverage
|
|
vitest run --coverage
|
|
|
|
# Jest with coverage
|
|
jest --coverage --coverageThreshold='{"global":{"branches":80,"functions":80,"lines":80}}'
|
|
```
|
|
|
|
## CI/CD Testing Pipeline
|
|
|
|
```yaml
|
|
# 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
|
|
|
|
1. **Arrange-Act-Assert (AAA) Pattern**
|
|
2. **One assertion per test** (when practical)
|
|
3. **Descriptive test names** - `should throw error when email is invalid`
|
|
4. **Test edge cases** - Empty inputs, boundary values, null/undefined
|
|
5. **Clean test data** - Reset database state between tests
|
|
6. **Fast tests** - Unit tests < 10ms, Integration < 100ms
|
|
7. **Deterministic** - No flaky tests, avoid sleep(), use waitFor()
|
|
8. **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/
|