π§ͺ Testing Pyramid
Unit Tests (70%)
Fast, isolated, test individual functions
Integration Tests (20%)
Test component interactions
E2E Tests (10%)
Test complete user workflows
β Unit Testing with Jest/Vitest
// sum.js
export function sum(a, b) {
return a + b
}
// sum.test.js
import { describe, it, expect } from 'vitest'
import { sum } from './sum'
describe('sum function', () => {
it('should add two numbers correctly', () => {
expect(sum(2, 3)).toBe(5)
})
it('should handle negative numbers', () => {
expect(sum(-1, 1)).toBe(0)
})
it('should handle zero', () => {
expect(sum(0, 5)).toBe(5)
})
})
// Run tests
npm testMocking
import { vi } from 'vitest'
// Mock function
const mockFn = vi.fn()
mockFn('hello')
expect(mockFn).toHaveBeenCalledWith('hello')
// Mock module
vi.mock('./api', () => ({
fetchUser: vi.fn(() => Promise.resolve({ id: 1, name: 'John' }))
}))
// Mock fetch
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: 'test' })
})
)π Integration Testing
// API Integration Test
import request from 'supertest'
import app from './app'
describe('POST /api/users', () => {
it('should create a new user', async () => {
const response = await request(app)
.post('/api/users')
.send({
name: 'John Doe',
email: 'john@example.com'
})
.expect(201)
expect(response.body).toHaveProperty('id')
expect(response.body.name).toBe('John Doe')
})
it('should return 400 for invalid email', async () => {
await request(app)
.post('/api/users')
.send({
name: 'John',
email: 'invalid-email'
})
.expect(400)
})
})π E2E Testing with Playwright
// tests/login.spec.js
import { test, expect } from '@playwright/test'
test('user can login successfully', async ({ page }) => {
// Navigate to login page
await page.goto('http://localhost:3000/login')
// Fill in form
await page.fill('input[name="email"]', 'user@example.com')
await page.fill('input[name="password"]', 'password123')
// Click login button
await page.click('button[type="submit"]')
// Wait for navigation
await page.waitForURL('**/dashboard')
// Verify user is logged in
await expect(page.locator('h1')).toContainText('Dashboard')
await expect(page.locator('.user-name')).toContainText('John Doe')
})
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('http://localhost:3000/login')
await page.fill('input[name="email"]', 'wrong@example.com')
await page.fill('input[name="password"]', 'wrongpass')
await page.click('button[type="submit"]')
await expect(page.locator('.error-message')).toContainText('Invalid credentials')
})
// Run tests
npx playwright testβοΈ React Component Testing
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import Button from './Button'
describe('Button component', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
it('calls onClick when clicked', () => {
const handleClick = vi.fn()
render(<Button onClick={handleClick}>Click</Button>)
fireEvent.click(screen.getByText('Click'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('is disabled when disabled prop is true', () => {
render(<Button disabled>Click</Button>)
expect(screen.getByRole('button')).toBeDisabled()
})
})π― Test-Driven Development (TDD)
- Red: Write a failing test
- Green: Write minimal code to pass
- Refactor: Improve code while keeping tests green
// 1. Red - Write failing test
test('should validate email format', () => {
expect(isValidEmail('test@example.com')).toBe(true)
expect(isValidEmail('invalid')).toBe(false)
})
// 2. Green - Implement function
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
// 3. Refactor - Improve if needed
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}π Code Coverage
// package.json
{
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage"
}
}
// Run with coverage
npm run test:coverage
// Coverage Report
File | % Stmts | % Branch | % Funcs | % Lines
----------|---------|----------|---------|--------
All files | 85.5 | 78.2 | 90.1 | 84.8
utils.js | 92.3 | 85.7 | 100 | 91.2
api.js | 78.9 | 70.5 | 80.0 | 78.3
// Aim for:
β’ 80%+ overall coverage
β’ 100% for critical paths
β’ Don't chase 100% blindlyβ Testing Best Practices
- Test behavior, not implementation: Focus on what, not how
- One assertion per test: Keep tests focused
- Descriptive test names: "should do X when Y"
- Arrange-Act-Assert: Clear test structure
- Independent tests: No shared state
- Fast tests: Unit tests should be milliseconds
- Mock external dependencies: APIs, databases
- Test edge cases: Empty, null, boundary values
π Testing Checklist
- β Unit tests for business logic
- β Integration tests for APIs
- β E2E tests for critical flows
- β Test edge cases and errors
- β Mock external dependencies
- β 80%+ code coverage
- β Tests run in CI/CD
- β Fast test execution
- β Clear test names
- β No flaky tests
π― Conclusion
Testing is not optionalβit's essential for reliable software. Follow the testing pyramid, write tests first (TDD), and make testing part of your development workflow. Good tests give you confidence to refactor and ship faster.
π§ Test Your Code
Use our developer tools to test and validate your code, APIs, and data formats.
Explore Tools β