Why Testing is Important
Testing is an important means of ensuring code quality and preventing regressions.
Code without tests:
- Makes refactoring scary
- Makes it unclear what changes affect
- Leads to bugs being discovered in production
The Test Pyramid
A model showing types of tests and their recommended ratios.
flowchart TB
subgraph Pyramid["Test Pyramid"]
E2E["E2E (10%)<br/>High cost, slow, unstable"]
Integration["Integration (20%)<br/>Medium cost, medium speed"]
Unit["Unit Tests (70%)<br/>Low cost, fast, stable"]
E2E --> Integration --> Unit
end
Unit Tests
Testing individual functions or classes in isolation.
Characteristics
| Item | Unit Test |
|---|---|
| Target | Functions, classes, modules |
| Speed | Very fast (ms) |
| Stability | High |
| Coverage | Narrow (single function) |
Implementation Example
// Test target
function calculateTotal(items, taxRate) {
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
return Math.round(subtotal * (1 + taxRate));
}
// Test
describe('calculateTotal', () => {
it('calculates total amount for products', () => {
const items = [
{ price: 100, quantity: 2 },
{ price: 200, quantity: 1 }
];
expect(calculateTotal(items, 0.1)).toBe(440);
});
it('returns 0 for empty array', () => {
expect(calculateTotal([], 0.1)).toBe(0);
});
it('returns subtotal when tax rate is 0%', () => {
const items = [{ price: 100, quantity: 1 }];
expect(calculateTotal(items, 0)).toBe(100);
});
});
AAA Pattern
it('creates a user', () => {
// Arrange
const userData = { name: 'Alice', email: 'alice@example.com' };
// Act
const user = createUser(userData);
// Assert
expect(user.id).toBeDefined();
expect(user.name).toBe('Alice');
});
Integration Tests
Testing that multiple components work together.
Characteristics
| Item | Integration Test |
|---|---|
| Target | API, database integration, external services |
| Speed | Moderate (seconds) |
| Stability | Moderate |
| Coverage | Moderate |
Implementation Example (API)
describe('POST /api/users', () => {
beforeEach(async () => {
await db.users.deleteMany();
});
it('creates a new user', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Alice', email: 'alice@example.com' })
.expect(201);
expect(response.body.id).toBeDefined();
expect(response.body.name).toBe('Alice');
// Confirm saved to DB
const user = await db.users.findById(response.body.id);
expect(user).not.toBeNull();
});
it('returns error for duplicate email address', async () => {
await db.users.create({ name: 'Bob', email: 'alice@example.com' });
await request(app)
.post('/api/users')
.send({ name: 'Alice', email: 'alice@example.com' })
.expect(409);
});
});
E2E Tests (End-to-End Tests)
Testing the entire application from the user’s perspective.
Characteristics
| Item | E2E Test |
|---|---|
| Target | Entire user flow |
| Speed | Slow (minutes) |
| Stability | Low (prone to flakiness) |
| Coverage | Wide |
Implementation Example (Playwright)
import { test, expect } from '@playwright/test';
test('login and display dashboard', async ({ page }) => {
// Navigate to login page
await page.goto('/login');
// Fill form
await page.fill('[name="email"]', 'user@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
// Confirm redirect to dashboard
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Dashboard');
});
test('search and purchase product', async ({ page }) => {
await page.goto('/');
// Search
await page.fill('[name="search"]', 'laptop');
await page.click('button[type="submit"]');
// Add product to cart
await page.click('[data-testid="add-to-cart"]');
// Go to cart
await page.click('[data-testid="cart-icon"]');
await expect(page.locator('.cart-item')).toHaveCount(1);
});
Mocks/Stubs
Simulating external dependencies to isolate tests.
// Mock external API
jest.mock('./paymentService', () => ({
processPayment: jest.fn().mockResolvedValue({ success: true, transactionId: 'tx_123' })
}));
import { processPayment } from './paymentService';
it('processes payment', async () => {
const result = await checkout(order);
expect(processPayment).toHaveBeenCalledWith({
amount: order.total,
currency: 'JPY'
});
expect(result.transactionId).toBe('tx_123');
});
Test-Driven Development (TDD)
A development method where you write tests before implementation.
flowchart LR
Red["Red<br/>(Failing test)"] --> Green["Green<br/>(Minimal implementation)"] --> Refactor["Refactor<br/>(Improve code)"] --> Red
TDD Example
// 1. Red: Write a failing test
it('throws error when password is less than 8 characters', () => {
expect(() => validatePassword('1234567')).toThrow('Password too short');
});
// 2. Green: Minimal implementation
function validatePassword(password) {
if (password.length < 8) {
throw new Error('Password too short');
}
}
// 3. Refactor: Improve code as needed
Coverage
A metric showing how much of your code is covered by tests.
| Coverage Type | Description |
|---|---|
| Line coverage | Percentage of lines executed |
| Branch coverage | Percentage of branches executed |
| Function coverage | Percentage of functions executed |
Coverage Guidelines
80% is often the target
| Consideration |
|---|
| No need to aim for 100% |
| High coverage ≠ high test quality |
| Prioritize covering important paths |
Choosing a Testing Strategy
| Scenario | Emphasized Test |
|---|---|
| Complex business logic | Unit tests |
| Many external integrations | Integration tests |
| UI is important | E2E tests |
| Legacy code refactoring | E2E as safety net |
Summary
An effective testing strategy is about balancing the test pyramid and distributing each level of tests appropriately. Focus on unit tests, verify integrations with integration tests, and guarantee important user flows with E2E tests. Testing is not something that slows down development, but an investment that improves long-term quality and development efficiency.
← Back to list