Master the art of testing JavaScript applications using modern tools and best practices. Learn how to write effective tests that ensure your code's reliability and maintainability. ๐
Setting Up Testing Tools ๐ฏ
# Install testing dependencies
npm install -D jest @types/jest
npm install -D @testing-library/react @testing-library/jest-dom
npm install -D cypress @cypress/code-coverage
npm install -D vitest @vitest/coverage-c8
Unit Testing with Jest ๐งช
// src/utils/math.ts
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
export function divide(a: number, b: number): number {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
// src/utils/math.test.ts
import { add, multiply, divide } from './math';
describe('Math utilities', () => {
describe('add', () => {
it('adds two positive numbers correctly', () => {
expect(add(2, 3)).toBe(5);
});
it('handles negative numbers', () => {
expect(add(-2, 3)).toBe(1);
expect(add(2, -3)).toBe(-1);
expect(add(-2, -3)).toBe(-5);
});
});
describe('multiply', () => {
it('multiplies two positive numbers correctly', () => {
expect(multiply(2, 3)).toBe(6);
});
it('handles zero', () => {
expect(multiply(5, 0)).toBe(0);
expect(multiply(0, 5)).toBe(0);
});
it('handles negative numbers', () => {
expect(multiply(-2, 3)).toBe(-6);
expect(multiply(2, -3)).toBe(-6);
expect(multiply(-2, -3)).toBe(6);
});
});
describe('divide', () => {
it('divides two numbers correctly', () => {
expect(divide(6, 2)).toBe(3);
});
it('handles decimal results', () => {
expect(divide(5, 2)).toBe(2.5);
});
it('throws error when dividing by zero', () => {
expect(() => divide(5, 0)).toThrow('Division by zero');
});
});
});
Testing React Components ๐จ
// src/components/Counter.tsx
import React, { useState } from 'react';
interface CounterProps {
initialValue?: number;
onCountChange?: (count: number) => void;
}
export const Counter: React.FC<CounterProps> = ({
initialValue = 0,
onCountChange
}) => {
const [count, setCount] = useState(initialValue);
const handleIncrement = () => {
const newCount = count + 1;
setCount(newCount);
onCountChange?.(newCount);
};
const handleDecrement = () => {
const newCount = count - 1;
setCount(newCount);
onCountChange?.(newCount);
};
return (
<div>
<h2>Counter: {count}</h2>
<button onClick={handleDecrement}>Decrease</button>
<button onClick={handleIncrement}>Increase</button>
</div>
);
};
// src/components/Counter.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Counter } from './Counter';
describe('Counter', () => {
it('renders with initial value', () => {
render(<Counter initialValue={5} />);
expect(screen.getByText('Counter: 5')).toBeInTheDocument();
});
it('increments counter when increase button is clicked', () => {
render(<Counter />);
fireEvent.click(screen.getByText('Increase'));
expect(screen.getByText('Counter: 1')).toBeInTheDocument();
});
it('decrements counter when decrease button is clicked', () => {
render(<Counter initialValue={5} />);
fireEvent.click(screen.getByText('Decrease'));
expect(screen.getByText('Counter: 4')).toBeInTheDocument();
});
it('calls onCountChange when value changes', () => {
const handleChange = jest.fn();
render(<Counter onCountChange={handleChange} />);
fireEvent.click(screen.getByText('Increase'));
expect(handleChange).toHaveBeenCalledWith(1);
fireEvent.click(screen.getByText('Decrease'));
expect(handleChange).toHaveBeenCalledWith(0);
});
});
Testing Async Code ๐
// src/services/api.ts
export interface User {
id: number;
name: string;
email: string;
}
export class ApiService {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async getUser(id: number): Promise<User> {
const response = await fetch(`${this.baseUrl}/users/${id}`);
if (!response.ok) {
throw new Error('User not found');
}
return response.json();
}
async createUser(user: Omit<User, 'id'>): Promise<User> {
const response = await fetch(`${this.baseUrl}/users`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(user)
});
if (!response.ok) {
throw new Error('Failed to create user');
}
return response.json();
}
}
// src/services/api.test.ts
import { ApiService } from './api';
describe('ApiService', () => {
let api: ApiService;
beforeEach(() => {
api = new ApiService('https://api.example.com');
global.fetch = jest.fn();
});
afterEach(() => {
jest.resetAllMocks();
});
describe('getUser', () => {
it('fetches user successfully', async () => {
const mockUser = {
id: 1,
name: 'John Doe',
email: 'john@example.com'
};
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockUser
});
const user = await api.getUser(1);
expect(user).toEqual(mockUser);
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/users/1'
);
});
it('handles user not found', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: false
});
await expect(api.getUser(999)).rejects.toThrow('User not found');
});
});
describe('createUser', () => {
it('creates user successfully', async () => {
const newUser = {
name: 'Jane Doe',
email: 'jane@example.com'
};
const mockResponse = {
id: 2,
...newUser
};
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockResponse
});
const user = await api.createUser(newUser);
expect(user).toEqual(mockResponse);
expect(global.fetch).toHaveBeenCalledWith(
'https://api.example.com/users',
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newUser)
}
);
});
});
});
Integration Testing ๐
// src/components/UserList.tsx
import React, { useEffect, useState } from 'react';
import { ApiService, User } from '../services/api';
interface UserListProps {
api: ApiService;
}
export const UserList: React.FC<UserListProps> = ({ api }) => {
const [users, setUsers] = useState<User[]>([]);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchUsers = async () => {
setLoading(true);
try {
const response = await fetch('/api/users');
const data = await response.json();
setUsers(data);
setError(null);
} catch (err) {
setError('Failed to fetch users');
} finally {
setLoading(false);
}
};
fetchUsers();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h2>Users</h2>
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
</div>
);
};
// src/components/UserList.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { UserList } from './UserList';
import { ApiService } from '../services/api';
describe('UserList', () => {
let api: ApiService;
beforeEach(() => {
api = new ApiService('https://api.example.com');
});
it('displays loading state initially', () => {
render(<UserList api={api} />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('displays users after successful fetch', async () => {
const mockUsers = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Doe', email: 'jane@example.com' }
];
global.fetch = jest.fn().mockResolvedValueOnce({
ok: true,
json: async () => mockUsers
});
render(<UserList api={api} />);
await waitFor(() => {
expect(screen.getByText('John Doe (john@example.com)')).toBeInTheDocument();
expect(screen.getByText('Jane Doe (jane@example.com)')).toBeInTheDocument();
});
});
it('displays error message on fetch failure', async () => {
global.fetch = jest.fn().mockRejectedValueOnce(new Error('API Error'));
render(<UserList api={api} />);
await waitFor(() => {
expect(screen.getByText('Error: Failed to fetch users')).toBeInTheDocument();
});
});
});
E2E Testing with Cypress ๐ฒ
// cypress/e2e/auth.cy.ts
describe('Authentication', () => {
beforeEach(() => {
cy.visit('/login');
});
it('successfully logs in', () => {
cy.intercept('POST', '/api/login', {
statusCode: 200,
body: {
token: 'fake-jwt-token',
user: {
id: 1,
name: 'Test User'
}
}
}).as('loginRequest');
cy.get('[data-testid=email-input]')
.type('test@example.com');
cy.get('[data-testid=password-input]')
.type('password123');
cy.get('[data-testid=login-button]')
.click();
cy.wait('@loginRequest');
cy.url().should('include', '/dashboard');
cy.get('[data-testid=user-name]')
.should('contain', 'Test User');
});
it('displays error message for invalid credentials', () => {
cy.intercept('POST', '/api/login', {
statusCode: 401,
body: {
error: 'Invalid credentials'
}
}).as('loginRequest');
cy.get('[data-testid=email-input]')
.type('wrong@example.com');
cy.get('[data-testid=password-input]')
.type('wrongpassword');
cy.get('[data-testid=login-button]')
.click();
cy.wait('@loginRequest');
cy.get('[data-testid=error-message]')
.should('contain', 'Invalid credentials');
});
});
// cypress/e2e/user-management.cy.ts
describe('User Management', () => {
beforeEach(() => {
cy.login(); // Custom command
cy.visit('/users');
});
it('creates a new user', () => {
cy.intercept('POST', '/api/users', {
statusCode: 201,
body: {
id: 123,
name: 'New User',
email: 'new@example.com'
}
}).as('createUser');
cy.get('[data-testid=add-user-button]')
.click();
cy.get('[data-testid=name-input]')
.type('New User');
cy.get('[data-testid=email-input]')
.type('new@example.com');
cy.get('[data-testid=submit-button]')
.click();
cy.wait('@createUser');
cy.get('[data-testid=user-list]')
.should('contain', 'New User');
});
});
Test Coverage ๐
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/index.tsx',
'!src/serviceWorker.ts'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
// cypress.config.ts
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
supportFile: 'cypress/support/e2e.ts',
specPattern: 'cypress/e2e/**/*.cy.ts',
video: false,
screenshotOnRunFailure: false,
setupNodeEvents(on, config) {
require('@cypress/code-coverage/task')(on, config);
return config;
}
}
});
Best Practices ๐
- Follow the Testing Trophy pattern
- Write meaningful test descriptions
- Use proper test isolation
- Implement proper mocking
- Test edge cases
- Maintain test coverage
- Use proper assertions
- Follow testing best practices
- Write maintainable tests
- Test business logic thoroughly
Additional Resources
Testing is a crucial part of software development that ensures code quality and reliability. This guide provides a solid foundation for implementing comprehensive testing strategies in JavaScript applications.