Testing Strategies - Unit, Integration, and E2E Testing Best Practices for Production Quality
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:
- Fast (milliseconds)
- Isolated (no external dependencies)
- Repeatable (same result every time)
- Self-validating (pass/fail, no manual inspection)
- 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.
Related Articles
GraphQL API Design - Production Architecture and Best Practices for Scalable Systems
Master GraphQL API design covering schema design principles, resolver optimization, N+1 query prevention with DataLoader, authentication and authorization patterns, caching strategies, error handling, and production deployment for high-performance GraphQL systems.
Testing Strategies - Unit, Integration, and E2E Testing Best Practices for Production Quality
Comprehensive guide to testing strategies covering unit tests, integration tests, end-to-end testing, test-driven development, mocking patterns, testing pyramid, and production testing practices for reliable software delivery.
Monitoring and Observability - Production Systems Performance and Debugging at Scale
Master monitoring and observability covering metrics collection with Prometheus, distributed tracing with OpenTelemetry, log aggregation, alerting strategies, SLOs/SLIs, and production debugging techniques for reliable systems.
Written by StaticBlock
StaticBlock is a technical writer and software engineer specializing in web development, performance optimization, and developer tooling.