The Testing Pyramid
Unit Testing
Setting Up Unit Tests
Unit Test Setup
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { UserService } from '../services/UserService';
// Mock external dependencies
vi.mock('../api/userApi', () => ({
fetchUser: vi.fn(),
updateUser: vi.fn()
}));
describe('UserService', () => {
let userService: UserService;
beforeEach(() => {
userService = new UserService();
});
describe('createUser', () => {
it('should create a user with valid data', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com'
};
const result = await userService.createUser(userData);
expect(result).toMatchObject({
id: expect.any(String),
name: userData.name,
email: userData.email
});
});
});
});Testing React Components
Component Testing
import { render, screen, fireEvent } from '@testing-library/react';
import { UserCard } from '../components/UserCard';
describe('UserCard', () => {
const mockUser = {
id: '1',
name: 'John Doe',
email: 'john@example.com',
avatar: 'https://example.com/avatar.png'
};
it('should render user information', () => {
render(<UserCard user={mockUser} />);
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
it('should call onEdit when edit button is clicked', () => {
const mockOnEdit = vi.fn();
render(<UserCard user={mockUser} onEdit={mockOnEdit} />);
fireEvent.click(screen.getByRole('button', { name: /edit/i }));
expect(mockOnEdit).toHaveBeenCalledWith(mockUser);
});
});Integration Testing
API Integration Tests
API Integration Tests
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import request from 'supertest';
import app from '../app';
describe('User API Integration', () => {
beforeEach(async () => {
await setupTestDatabase();
});
afterEach(async () => {
await cleanupTestDatabase();
});
describe('POST /api/users', () => {
it('should create a new user', async () => {
const userData = {
name: 'John Doe',
email: 'john@example.com',
password: 'password123'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(String),
name: userData.name,
email: userData.email
});
expect(response.body.password).toBeUndefined();
});
});
});End-to-End Testing
Playwright E2E Tests
E2E Tests
import { test, expect } from '@playwright/test';
test.describe('User Management', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'admin@example.com');
await page.fill('[data-testid="password"]', 'password123');
await page.click('[data-testid="login-button"]');
await expect(page).toHaveURL('/dashboard');
});
test('should create a new user', async ({ page }) => {
await page.goto('/users');
// Click create user button
await page.click('[data-testid="create-user-button"]');
// Fill user form
await page.fill('[data-testid="user-name"]', 'John Doe');
await page.fill('[data-testid="user-email"]', 'john@example.com');
await page.fill('[data-testid="user-password"]', 'password123');
// Submit form
await page.click('[data-testid="submit-button"]');
// Verify user was created
await expect(page.locator('[data-testid="user-list"]')).toContainText('John Doe');
});
});Test-Driven Development (TDD)
TDD Example
TDD Example
// 1. Write a failing test
describe('UserService', () => {
describe('validateEmail', () => {
it('should return true for valid email', () => {
const userService = new UserService();
expect(userService.validateEmail('test@example.com')).toBe(true);
});
it('should return false for invalid email', () => {
const userService = new UserService();
expect(userService.validateEmail('invalid-email')).toBe(false);
});
});
});
// 2. Write minimal code to make test pass
export class UserService {
validateEmail(email: string): boolean {
const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
return emailRegex.test(email);
}
}
// 3. Refactor if needed
// 4. Repeat the cycleMocking and Stubbing
Mocking External Dependencies
Mocking
import { vi } from 'vitest';
// Mock external API
vi.mock('../services/apiService', () => ({
fetchUser: vi.fn(),
updateUser: vi.fn()
}));
// Mock database
vi.mock('../database/connection', () => ({
query: vi.fn()
}));
describe('UserService with mocks', () => {
it('should fetch user from API', async () => {
const mockFetchUser = vi.mocked(fetchUser);
mockFetchUser.mockResolvedValue({
id: '1',
name: 'John Doe',
email: 'john@example.com'
});
const userService = new UserService();
const user = await userService.getUser('1');
expect(mockFetchUser).toHaveBeenCalledWith('1');
expect(user).toMatchObject({
id: '1',
name: 'John Doe',
email: 'john@example.com'
});
});
});Performance Testing
Load Testing
Load Testing
# Load testing with Artillery
config:
target: 'http://localhost:3000'
phases:
- duration: 60
arrivalRate: 10
- duration: 120
arrivalRate: 20
scenarios:
- name: "User API Load Test"
weight: 70
flow: - post:
url: "/api/users"
json:
name: "Test User {{ $randomString() }}"
email: "test{{ $randomString() }}@example.com"
password: "password123"Best Practices
- Write tests first - Use TDD when possible
- Test behavior, not implementation - Focus on what the code does
- Keep tests simple and focused - One test should verify one behavior
- Use descriptive test names - Make it clear what each test verifies
- Mock external dependencies - Isolate the code under test
- Maintain good test coverage - Aim for 80%+ coverage
- Run tests frequently - Integrate testing into your development workflow
- Keep tests fast - Unit tests should run in milliseconds
- Make tests reliable - Avoid flaky tests
- Document complex test scenarios - Add comments for complex logic