Files
english/.opencode/skills/backend-development/references/backend-testing.md
2026-04-12 01:06:31 +07:00

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

  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