The Testing Pyramid
A balanced testing strategy combines unit tests (fast, many), integration tests (medium), and E2E tests (slow, few). Each type catches different bugs.
Unit Testing with Jest
// Pure function test
describe('calculateTotal', () => {
it('calculates total with tax', () => {
const items = [
{ price: 100, quantity: 2 },
{ price: 50, quantity: 1 }
];
expect(calculateTotal(items, 0.1)).toBe(275); // (200 + 50) * 1.1
});
it('returns 0 for empty cart', () => {
expect(calculateTotal([], 0.1)).toBe(0);
});
});
// Mocking dependencies
jest.mock('./api');
import { fetchUser } from './api';
describe('UserService', () => {
it('fetches and transforms user data', async () => {
(fetchUser as jest.Mock).mockResolvedValue({
id: '1',
first_name: 'John',
last_name: 'Doe'
});
const user = await userService.getUser('1');
expect(user).toEqual({
id: '1',
fullName: 'John Doe'
});
});
});Component Testing with Testing Library
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('LoginForm', () => {
it('shows validation errors for empty fields', async () => {
render();
await userEvent.click(screen.getByRole('button', { name: /login/i }));
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
});
it('calls onSubmit with form data', async () => {
const handleSubmit = jest.fn();
render();
await userEvent.type(
screen.getByLabelText(/email/i),
'test@example.com'
);
await userEvent.type(
screen.getByLabelText(/password/i),
'password123'
);
await userEvent.click(screen.getByRole('button', { name: /login/i }));
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
it('disables button while loading', async () => {
render();
expect(screen.getByRole('button', { name: /loading/i })).toBeDisabled();
});
}); Angular Testing
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
describe('UserListComponent', () => {
let component: UserListComponent;
let fixture: ComponentFixture;
let userService: jasmine.SpyObj;
beforeEach(async () => {
const spy = jasmine.createSpyObj('UserService', ['getUsers']);
await TestBed.configureTestingModule({
imports: [UserListComponent, HttpClientTestingModule],
providers: [{ provide: UserService, useValue: spy }]
}).compileComponents();
userService = TestBed.inject(UserService) as jasmine.SpyObj;
fixture = TestBed.createComponent(UserListComponent);
component = fixture.componentInstance;
});
it('displays users from service', async () => {
userService.getUsers.and.returnValue(of([
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' }
]));
fixture.detectChanges();
await fixture.whenStable();
const items = fixture.nativeElement.querySelectorAll('.user-item');
expect(items.length).toBe(2);
});
}); E2E Testing with Playwright
import { test, expect } from '@playwright/test';
test.describe('Authentication', () => {
test('user can login successfully', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'user@example.com');
await page.fill('[data-testid="password"]', 'password123');
await page.click('[data-testid="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toHaveText('Welcome back!');
});
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'wrong@example.com');
await page.fill('[data-testid="password"]', 'wrongpassword');
await page.click('[data-testid="submit"]');
await expect(page.locator('.error-message')).toHaveText(
'Invalid email or password'
);
});
});
test.describe('Shopping Cart', () => {
test.beforeEach(async ({ page }) => {
// Login before each test
await page.goto('/login');
await page.fill('[data-testid="email"]', 'user@example.com');
await page.fill('[data-testid="password"]', 'password123');
await page.click('[data-testid="submit"]');
});
test('user can add items to cart', async ({ page }) => {
await page.goto('/products');
await page.click('[data-testid="add-to-cart-1"]');
const cartCount = page.locator('[data-testid="cart-count"]');
await expect(cartCount).toHaveText('1');
});
});API Mocking with MSW
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('/api/users', (req, res, ctx) => {
return res(ctx.json([
{ id: '1', name: 'John' },
{ id: '2', name: 'Jane' }
]));
}),
rest.post('/api/login', async (req, res, ctx) => {
const { email, password } = await req.json();
if (email === 'valid@example.com' && password === 'password') {
return res(ctx.json({ token: 'fake-token' }));
}
return res(ctx.status(401), ctx.json({ error: 'Invalid credentials' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());Testing Strategy
| Type | Ratio | Focus |
|---|---|---|
| Unit | 70% | Logic, utilities, services |
| Integration | 20% | Components, hooks, state |
| E2E | 10% | Critical user journeys |
Conclusion
Good tests give confidence to refactor and ship. Focus on testing behavior, not implementation. Use the right tool for each level of the testing pyramid.