0% read
Skip to main content
Testing Strategies - Unit, Integration, and E2E Testing Best Practices for Production Quality

Testing Strategies - Unit, Integration, and E2E Testing Best Practices for Production Quality

S
StaticBlock
23 min read

Testing is fundamental to delivering reliable software at scale. Companies like Google run billions of tests daily, Netflix deploys to production 4,000+ times per day with confidence through comprehensive testing, and Spotify maintains 99.9% uptime by catching bugs before they reach users.

This guide covers testing strategies, the testing pyramid, unit testing best practices, integration testing patterns, end-to-end testing with Playwright/Cypress, test-driven development, mocking and stubbing, testing in production, and quality metrics used by high-performing engineering teams.

Table of Contents

The Testing Pyramid

Understanding the Testing Pyramid

The testing pyramid is a framework for balancing different types of tests:

     /\
    /E2E\         ← Few (Slow, Expensive, Brittle)
   /------\
  /  INT   \      ← Some (Medium Speed, Medium Cost)
 /----------\
/   UNIT     \    ← Many (Fast, Cheap, Stable)
--------------

Recommended Ratios:

  • Unit Tests: 70% (fast, isolated, many)
  • Integration Tests: 20% (medium speed, test component interactions)
  • End-to-End Tests: 10% (slow, test critical user journeys)

Why This Balance?

// Unit Test: 1-5ms execution time
test('calculates total price', () => {
  expect(calculateTotal([10, 20, 30])).toBe(60);
});

// Integration Test: 50-200ms execution time test('order service creates order and updates inventory', async () => { const order = await orderService.create({ items: [...] }); const inventory = await inventoryService.getStock(order.items[0].id); expect(inventory.quantity).toBe(9); // Started at 10 });

// E2E Test: 2-10 seconds execution time test('user completes checkout flow', async () => { await page.goto('https://example.com'); await page.click('[data-testid="add-to-cart"]'); await page.click('[data-testid="checkout"]'); await page.fill('#credit-card', '4242424242424242'); await page.click('[data-testid="submit-order"]'); await expect(page.locator('.order-confirmation')).toBeVisible(); });

Cost Analysis:

  • 1,000 unit tests run in ~5 seconds
  • 1,000 integration tests run in ~3 minutes
  • 1,000 E2E tests run in ~3 hours

Unit Testing Best Practices

Writing Effective Unit Tests

Characteristics of Good Unit Tests:

  1. Fast (milliseconds)
  2. Isolated (no external dependencies)
  3. Repeatable (same result every time)
  4. Self-validating (pass/fail, no manual inspection)
  5. Timely (written with or before code)

AAA Pattern (Arrange, Act, Assert):

// product.test.js
import { Product } from './product';

describe('Product', () => { test('applies discount correctly', () => { // Arrange - Set up test data const product = new Product({ name: 'Laptop', price: 1000 });

// Act - Execute the code under test
const discountedPrice = product.applyDiscount(0.2); // 20% off

// Assert - Verify the result
expect(discountedPrice).toBe(800);

});

test('throws error for invalid discount', () => { const product = new Product({ name: 'Mouse', price: 50 });

expect(() => {
  product.applyDiscount(1.5); // 150% discount invalid
}).toThrow('Discount must be between 0 and 1');

}); });

Testing Edge Cases and Boundaries

// validation.test.js
import { validateEmail } from './validation';

describe('validateEmail', () => { // Valid cases test('accepts valid email addresses', () => { expect(validateEmail('user@example.com')).toBe(true); expect(validateEmail('user+tag@example.co.uk')).toBe(true); expect(validateEmail('user.name@example.com')).toBe(true); });

// Invalid cases test('rejects invalid email addresses', () => { expect(validateEmail('invalid')).toBe(false); expect(validateEmail('invalid@')).toBe(false); expect(validateEmail('@example.com')).toBe(false); expect(validateEmail('user @example.com')).toBe(false); // Space });

// Edge cases test('handles edge cases', () => { expect(validateEmail('')).toBe(false); // Empty string expect(validateEmail(null)).toBe(false); // Null expect(validateEmail(undefined)).toBe(false); // Undefined expect(validateEmail('a@b.c')).toBe(true); // Minimal valid });

// Boundary cases test('validates email length limits', () => { const longEmail = 'a'.repeat(64) + '@' + 'b'.repeat(255) + '.com'; expect(validateEmail(longEmail)).toBe(false); // Exceeds max length }); });

Testing Async Code

// user-service.test.js
import { UserService } from './user-service';

describe('UserService', () => { let userService;

beforeEach(() => { userService = new UserService(); });

// Using async/await test('fetches user by id', async () => { const user = await userService.getUser('user-123');

expect(user).toMatchObject({
  id: 'user-123',
  email: 'alice@example.com'
});

});

// Testing promises test('creates new user', () => { const userData = { email: 'bob@example.com', name: 'Bob' };

return userService.createUser(userData).then(user => {
  expect(user.id).toBeDefined();
  expect(user.email).toBe('bob@example.com');
});

});

// Testing rejected promises test('throws error for duplicate email', async () => { const userData = { email: 'alice@example.com' }; // Already exists

await expect(userService.createUser(userData))
  .rejects
  .toThrow('Email already exists');

}); });

Parameterized Tests

// math.test.js
import { add } from './math';

describe('add', () => { // Test multiple cases with same logic test.each([ [1, 1, 2], [2, 3, 5], [10, -5, 5], [0, 0, 0], [-5, -10, -15] ])('add(%i, %i) returns %i', (a, b, expected) => { expect(add(a, b)).toBe(expected); }); });

Mocking and Stubbing

When to Mock

Mock external dependencies:

  • Database queries
  • HTTP requests
  • File system operations
  • Date/time
  • Random number generation
// order-service.test.js
import { OrderService } from './order-service';
import { PaymentGateway } from './payment-gateway';

// Mock the payment gateway jest.mock('./payment-gateway');

describe('OrderService', () => { let orderService; let paymentGatewayMock;

beforeEach(() => { // Create fresh mocks for each test paymentGatewayMock = new PaymentGateway(); orderService = new OrderService(paymentGatewayMock); });

test('creates order and charges payment', async () => { // Setup mock behavior paymentGatewayMock.charge.mockResolvedValue({ transactionId: 'txn-123', status: 'success' });

const order = await orderService.createOrder({
  userId: 'user-123',
  items: [{ id: 'item-1', price: 99.99 }],
  total: 99.99
});

// Verify payment gateway was called correctly
expect(paymentGatewayMock.charge).toHaveBeenCalledWith({
  amount: 99.99,
  userId: 'user-123'
});

// Verify order was created
expect(order).toMatchObject({
  status: 'confirmed',
  transactionId: 'txn-123'
});

});

test('handles payment failure', async () => { // Mock payment failure paymentGatewayMock.charge.mockRejectedValue( new Error('Insufficient funds') );

await expect(
  orderService.createOrder({
    userId: 'user-123',
    total: 99.99
  })
).rejects.toThrow('Payment failed');

}); });

Mocking HTTP Requests

// api-client.test.js
import axios from 'axios';
import { ApiClient } from './api-client';

jest.mock('axios');

describe('ApiClient', () => { let client;

beforeEach(() => { client = new ApiClient('https://api.example.com'); });

test('fetches posts successfully', async () => { const mockPosts = [ { id: 1, title: 'Post 1' }, { id: 2, title: 'Post 2' } ];

axios.get.mockResolvedValue({ data: mockPosts });

const posts = await client.getPosts();

expect(axios.get).toHaveBeenCalledWith(
  'https://api.example.com/posts'
);
expect(posts).toEqual(mockPosts);

});

test('handles API errors', async () => { axios.get.mockRejectedValue(new Error('Network error'));

await expect(client.getPosts()).rejects.toThrow('Network error');

}); });

Mocking Dates and Time

// subscription.test.js
import { Subscription } from './subscription';

describe('Subscription', () => { beforeEach(() => { // Mock Date to fixed time jest.useFakeTimers(); jest.setSystemTime(new Date('2026-03-12T00:00:00Z')); });

afterEach(() => { jest.useRealTimers(); });

test('calculates expiration date correctly', () => { const subscription = new Subscription({ startDate: new Date(), durationMonths: 3 });

expect(subscription.expiresAt).toEqual(
  new Date('2026-06-12T00:00:00Z')
);

});

test('detects expired subscription', () => { const subscription = new Subscription({ startDate: new Date('2025-12-12'), durationMonths: 3 });

expect(subscription.isExpired()).toBe(true);

}); });

Integration Testing

Database Integration Tests

// user-repository.integration.test.js
import { UserRepository } from './user-repository';
import { setupTestDatabase, cleanupTestDatabase } from './test-utils';

describe('UserRepository Integration Tests', () => { let db; let userRepo;

beforeAll(async () => { // Setup test database db = await setupTestDatabase(); userRepo = new UserRepository(db); });

afterAll(async () => { await cleanupTestDatabase(db); });

beforeEach(async () => { // Clean tables before each test await db.query('TRUNCATE TABLE users CASCADE'); });

test('creates user in database', async () => { const userData = { email: 'alice@example.com', name: 'Alice Johnson' };

const user = await userRepo.create(userData);

expect(user.id).toBeDefined();
expect(user.email).toBe('alice@example.com');

// Verify in database
const rows = await db.query(
  'SELECT * FROM users WHERE id = $1',
  [user.id]
);

expect(rows[0]).toMatchObject({
  email: 'alice@example.com',
  name: 'Alice Johnson'
});

});

test('enforces unique email constraint', async () => { await userRepo.create({ email: 'alice@example.com', name: 'Alice' });

// Try to create duplicate
await expect(
  userRepo.create({ email: 'alice@example.com', name: 'Alice 2' })
).rejects.toThrow('Email already exists');

});

test('finds user by email', async () => { await userRepo.create({ email: 'bob@example.com', name: 'Bob' });

const user = await userRepo.findByEmail('bob@example.com');

expect(user).toMatchObject({
  email: 'bob@example.com',
  name: 'Bob'
});

}); });

API Integration Tests

// api.integration.test.js
import request from 'supertest';
import { app } from '../app';
import { setupTestDatabase, cleanupTestDatabase } from './test-utils';

describe('API Integration Tests', () => { let db;

beforeAll(async () => { db = await setupTestDatabase(); });

afterAll(async () => { await cleanupTestDatabase(db); });

beforeEach(async () => { await db.query('TRUNCATE TABLE users CASCADE'); });

describe('POST /api/users', () => { test('creates new user', async () => { const response = await request(app) .post('/api/users') .send({ email: 'alice@example.com', name: 'Alice', password: 'password123' }) .expect(201);

  expect(response.body).toMatchObject({
    id: expect.any(String),
    email: 'alice@example.com',
    name: 'Alice'
  });

  expect(response.body.password).toBeUndefined(); // Should not expose password
});

test('validates required fields', async () => {
  const response = await request(app)
    .post('/api/users')
    .send({ email: 'invalid' })
    .expect(400);

  expect(response.body.errors).toContain('Name is required');
  expect(response.body.errors).toContain('Password is required');
});

test('prevents duplicate email', async () => {
  await request(app)
    .post('/api/users')
    .send({ email: 'bob@example.com', name: 'Bob', password: 'pass' });

  const response = await request(app)
    .post('/api/users')
    .send({ email: 'bob@example.com', name: 'Bob 2', password: 'pass' })
    .expect(409);

  expect(response.body.error).toBe('Email already exists');
});

});

describe('GET /api/users/:id', () => { test('returns user by id', async () => { const createResponse = await request(app) .post('/api/users') .send({ email: 'charlie@example.com', name: 'Charlie', password: 'pass' });

  const userId = createResponse.body.id;

  const response = await request(app)
    .get(`/api/users/${userId}`)
    .expect(200);

  expect(response.body).toMatchObject({
    id: userId,
    email: 'charlie@example.com',
    name: 'Charlie'
  });
});

test('returns 404 for non-existent user', async () => {
  await request(app)
    .get('/api/users/non-existent-id')
    .expect(404);
});

}); });

End-to-End Testing

Playwright E2E Tests

// e2e/checkout.spec.js
import { test, expect } from '@playwright/test';

test.describe('Checkout Flow', () => { test.beforeEach(async ({ page }) => { // Setup: Navigate to product page await page.goto('https://example.com/products'); });

test('user completes checkout successfully', async ({ page }) => { // Add product to cart await page.click('[data-testid="product-1"]'); await page.click('[data-testid="add-to-cart"]');

// Verify cart badge updates
await expect(page.locator('.cart-badge')).toHaveText('1');

// Navigate to cart
await page.click('[data-testid="cart-icon"]');

// Verify product in cart
await expect(page.locator('.cart-item')).toHaveCount(1);
await expect(page.locator('.cart-total')).toContainText('$99.99');

// Proceed to checkout
await page.click('[data-testid="checkout-button"]');

// Fill shipping information
await page.fill('#name', 'Alice Johnson');
await page.fill('#email', 'alice@example.com');
await page.fill('#address', '123 Main St');
await page.fill('#city', 'San Francisco');
await page.selectOption('#country', 'US');
await page.fill('#zip', '94102');

// Fill payment information
await page.fill('#card-number', '4242424242424242');
await page.fill('#card-expiry', '12/28');
await page.fill('#card-cvc', '123');

// Submit order
await page.click('[data-testid="submit-order"]');

// Verify order confirmation
await expect(page.locator('.order-confirmation')).toBeVisible();
await expect(page.locator('.order-number')).toContainText(/ORD-\d+/);
await expect(page.locator('.confirmation-email')).toContainText('alice@example.com');

});

test('validates payment information', async ({ page }) => { // Add product and proceed to checkout await page.click('[data-testid="product-1"]'); await page.click('[data-testid="add-to-cart"]'); await page.click('[data-testid="cart-icon"]'); await page.click('[data-testid="checkout-button"]');

// Fill shipping info
await page.fill('#name', 'Bob Smith');
await page.fill('#email', 'bob@example.com');
await page.fill('#address', '456 Oak Ave');

// Try to submit with invalid card
await page.fill('#card-number', '1111');
await page.click('[data-testid="submit-order"]');

// Verify error message
await expect(page.locator('.error-message')).toContainText('Invalid card number');

});

test('handles empty cart', async ({ page }) => { await page.click('[data-testid="cart-icon"]');

await expect(page.locator('.empty-cart-message')).toBeVisible();
await expect(page.locator('[data-testid="checkout-button"]')).toBeDisabled();

}); });

Visual Regression Testing

// e2e/visual.spec.js
import { test, expect } from '@playwright/test';

test.describe('Visual Regression Tests', () => { test('homepage renders correctly', async ({ page }) => { await page.goto('https://example.com');

// Take screenshot and compare to baseline
await expect(page).toHaveScreenshot('homepage.png', {
  fullPage: true,
  maxDiffPixels: 100 // Allow up to 100 pixels difference
});

});

test('product card renders correctly', async ({ page }) => { await page.goto('https://example.com/products');

const productCard = page.locator('[data-testid="product-1"]');

await expect(productCard).toHaveScreenshot('product-card.png');

});

test('responsive layout on mobile', async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }); // iPhone SE await page.goto('https://example.com');

await expect(page).toHaveScreenshot('homepage-mobile.png', {
  fullPage: true
});

}); });

Test-Driven Development (TDD)

Red-Green-Refactor Cycle

1. Red: Write failing test

// calculator.test.js
import { Calculator } from './calculator';

describe('Calculator', () => { test('divides two numbers', () => { const calc = new Calculator();

expect(calc.divide(10, 2)).toBe(5);

});

test('throws error when dividing by zero', () => { const calc = new Calculator();

expect(() => calc.divide(10, 0)).toThrow('Cannot divide by zero');

}); });

// Test fails: Calculator.divide is not defined

2. Green: Write minimal code to pass

// calculator.js
export class Calculator {
  divide(a, b) {
    if (b === 0) {
      throw new Error('Cannot divide by zero');
    }
    return a / b;
  }
}

// Test passes ✓

3. Refactor: Improve code while keeping tests green

// calculator.js (refactored)
export class Calculator {
  divide(dividend, divisor) {
    this.validateDivisor(divisor);
    return dividend / divisor;
  }

validateDivisor(divisor) { if (divisor === 0) { throw new Error('Cannot divide by zero'); } } }

// Tests still pass ✓

TDD Example: Shopping Cart

// shopping-cart.test.js (Write tests first)
import { ShoppingCart } from './shopping-cart';

describe('ShoppingCart', () => { let cart;

beforeEach(() => { cart = new ShoppingCart(); });

test('starts empty', () => { expect(cart.items).toEqual([]); expect(cart.total).toBe(0); });

test('adds item to cart', () => { cart.addItem({ id: 'item-1', name: 'Laptop', price: 999 });

expect(cart.items).toHaveLength(1);
expect(cart.total).toBe(999);

});

test('adds multiple items', () => { cart.addItem({ id: 'item-1', price: 999 }); cart.addItem({ id: 'item-2', price: 499 });

expect(cart.items).toHaveLength(2);
expect(cart.total).toBe(1498);

});

test('removes item from cart', () => { cart.addItem({ id: 'item-1', price: 999 }); cart.addItem({ id: 'item-2', price: 499 });

cart.removeItem('item-1');

expect(cart.items).toHaveLength(1);
expect(cart.total).toBe(499);

});

test('applies discount code', () => { cart.addItem({ id: 'item-1', price: 1000 });

cart.applyDiscount('SAVE20'); // 20% off

expect(cart.total).toBe(800);
expect(cart.discount).toBe(200);

});

test('rejects invalid discount code', () => { cart.addItem({ id: 'item-1', price: 1000 });

expect(() => cart.applyDiscount('INVALID')).toThrow('Invalid discount code');

}); });

Implementation (written after tests):

// shopping-cart.js
export class ShoppingCart {
  constructor() {
    this.items = [];
    this.discount = 0;
    this.discountCodes = {
      'SAVE20': 0.2,
      'SAVE10': 0.1
    };
  }

get total() { const subtotal = this.items.reduce((sum, item) => sum + item.price, 0); return subtotal - this.discount; }

addItem(item) { this.items.push(item); }

removeItem(itemId) { this.items = this.items.filter(item => item.id !== itemId); }

applyDiscount(code) { const discountRate = this.discountCodes[code];

if (!discountRate) {
  throw new Error('Invalid discount code');
}

const subtotal = this.items.reduce((sum, item) => sum + item.price, 0);
this.discount = subtotal * discountRate;

} }

Testing Best Practices

Code Coverage

# Run tests with coverage
npm test -- --coverage

Example output:

File | % Stmts | % Branch | % Funcs | % Lines |

------------------|---------|----------|---------|---------|

calculator.js | 100 | 100 | 100 | 100 |

shopping-cart.js | 95 | 90 | 100 | 95 |

user-service.js | 80 | 75 | 85 | 80 |

Coverage Goals:

  • 80%+ overall coverage (minimum)
  • 100% coverage for critical paths (payment, authentication, data loss)
  • Focus on branch coverage, not just line coverage

Test Organization

// Good: Descriptive test names
describe('UserService', () => {
  describe('createUser', () => {
    test('creates user with hashed password', async () => { /* ... */ });
    test('throws error if email already exists', async () => { /* ... */ });
    test('sends welcome email after creation', async () => { /* ... */ });
  });

describe('authenticateUser', () => { test('returns user for valid credentials', async () => { /* ... / }); test('throws error for invalid password', async () => { / ... / }); test('throws error for non-existent email', async () => { / ... */ }); }); });

Continuous Integration

# .github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs: test: runs-on: ubuntu-latest

services:
  postgres:
    image: postgres:15
    env:
      POSTGRES_PASSWORD: postgres
    options: >-
      --health-cmd pg_isready
      --health-interval 10s
      --health-timeout 5s
      --health-retries 5

steps:
  - uses: actions/checkout@v3

  - name: Setup Node.js
    uses: actions/setup-node@v3
    with:
      node-version: '20'

  - name: Install dependencies
    run: npm ci

  - name: Run unit tests
    run: npm test

  - name: Run integration tests
    run: npm run test:integration
    env:
      DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test

  - name: Run E2E tests
    run: npm run test:e2e

  - name: Upload coverage
    uses: codecov/codecov-action@v3

Real-World Examples

Google: Testing at Scale

Test Infrastructure:

  • 4+ billion tests run daily
  • 150+ million test results per day
  • 99% of code changes include tests

Key Practices:

  • Test size classification (small <60s, medium <300s, large unlimited)
  • Hermetic tests (no external dependencies)
  • Flaky test detection and quarantine
  • Test result caching (50% faster CI)

Netflix: Testing in Production

Chaos Engineering:

  • Chaos Monkey randomly terminates instances
  • Chaos Kong simulates region failures
  • Tests system resilience under failure conditions

Feature Testing:

  • A/B testing for 125M+ members
  • 250+ experiments running concurrently
  • Automated rollback on metric regressions

Spotify: Quality Metrics

Key Metrics Tracked:

  • Test Coverage: 80%+ required
  • Build Time: <10 minutes (parallel execution)
  • Flaky Test Rate: <0.5%
  • Bug Escape Rate: <5% (bugs reaching production)
  • Mean Time to Recovery (MTTR): <30 minutes

Test Automation:

  • 95% of tests automated
  • Tests run on every commit
  • Automated canary deployments

Conclusion

Effective testing strategies are essential for delivering reliable software at scale. Key takeaways:

Testing Pyramid:

  • Write many unit tests (70%), some integration tests (20%), few E2E tests (10%)
  • Balance speed, cost, and confidence
  • Focus coverage on critical paths

Best Practices:

  • Write tests before or with code (TDD)
  • Keep tests fast, isolated, and repeatable
  • Mock external dependencies
  • Aim for 80%+ code coverage
  • Use descriptive test names
  • Run tests in CI/CD pipeline

Quality Metrics:

  • Track test coverage, build time, flaky test rate
  • Monitor bug escape rate and MTTR
  • Measure deployment frequency and success rate

Companies like Google, Netflix, and Spotify demonstrate that comprehensive testing enables rapid deployment velocity while maintaining high reliability. Invest in testing infrastructure early, automate aggressively, and treat tests as first-class code.

Found this helpful? Share it!

Related Articles

S

Written by StaticBlock

StaticBlock is a technical writer and software engineer specializing in web development, performance optimization, and developer tooling.