🎯 Exemples recommandés
Balanced sample collections from various categories for you to explore
Framework de Test Jest
Exemples complets de tests Jest incluant tests unitaires, tests d'intégration, mocking, tests asynchrones et patterns avancés pour applications JavaScript/TypeScript
💻 Configuration de Base Jest javascript
Configuration complète de projet Jest avec fichiers de configuration, structure de tests de base et utilitaires communs
// Jest Configuration Examples
// 1. jest.config.js - Basic configuration
module.exports = {
// Test environment
testEnvironment: 'jsdom',
// Test file patterns
testMatch: [
'**/__tests__/**/*.+(ts|tsx|js)',
'**/*.(test|spec).+(ts|tsx|js)'
],
// Coverage configuration
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.(ts|tsx|js)',
'!src/**/*.d.ts',
'!src/index.ts'
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
// Setup files
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
// Module path mapping
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1',
'\.(css|less|scss|sass)$': 'identity-obj-proxy'
},
// Transform configuration
transform: {
'^.+\.(ts|tsx)$': 'ts-jest',
'^.+\.(js|jsx)$': 'babel-jest'
},
// Test timeout
testTimeout: 10000,
// Verbose output
verbose: true
};
// 2. package.json scripts
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --ci --coverage --watchAll=false"
},
"devDependencies": {
"jest": "^29.7.0",
"@types/jest": "^29.5.8",
"ts-jest": "^29.1.1",
"babel-jest": "^29.7.0",
"@babel/preset-env": "^7.23.6"
}
}
// 3. src/setupTests.js - Global test setup
import '@testing-library/jest-dom';
// Mock global objects
global.fetch = jest.fn();
// Setup custom matchers
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
if (pass) {
return {
message: () =>
`expected ${received} not to be within range ${floor} - ${ceiling}`,
pass: true,
};
} else {
return {
message: () =>
`expected ${received} to be within range ${floor} - ${ceiling}`,
pass: false,
};
}
},
});
// 4. Basic test file structure
// src/utils/math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => {
if (b === 0) throw new Error('Division by zero');
return a / b;
};
// src/utils/__tests__/math.test.js
import { add, subtract, multiply, divide } from '../math';
describe('Math Utilities', () => {
describe('add', () => {
test('should add two positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('should add negative numbers', () => {
expect(add(-2, -3)).toBe(-5);
});
test('should handle zero', () => {
expect(add(0, 5)).toBe(5);
expect(add(5, 0)).toBe(5);
});
});
describe('subtract', () => {
test('should subtract two numbers', () => {
expect(subtract(10, 4)).toBe(6);
});
});
describe('multiply', () => {
test('should multiply two numbers', () => {
expect(multiply(3, 4)).toBe(12);
});
test('should handle multiplication by zero', () => {
expect(multiply(5, 0)).toBe(0);
});
});
describe('divide', () => {
test('should divide two numbers', () => {
expect(divide(10, 2)).toBe(5);
});
test('should throw error when dividing by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
});
💻 Tests Asynchrones et Promises javascript
Exemples complets de tests de code asynchrone, promises, async/await, timers et appels API
// Async and Promise Testing with Jest
// 1. Testing Promises
// src/api/userService.js
export const userService = {
async getUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error('User not found');
}
return response.json();
},
async createUser(userData) {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
return response.json();
},
getUsersWithDelay() {
return new Promise((resolve) => {
setTimeout(() => {
resolve([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]);
}, 1000);
});
}
};
// src/api/__tests__/userService.test.js
import { userService } from '../userService';
// Mock fetch globally
global.fetch = jest.fn();
describe('UserService - Async Tests', () => {
beforeEach(() => {
fetch.mockClear();
});
describe('getUser', () => {
test('should return user data when user exists', async () => {
const mockUser = { id: 1, name: 'John Doe', email: '[email protected]' };
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUser,
});
const result = await userService.getUser(1);
expect(fetch).toHaveBeenCalledWith('/api/users/1');
expect(result).toEqual(mockUser);
});
test('should throw error when user not found', async () => {
fetch.mockResolvedValueOnce({
ok: false,
});
await expect(userService.getUser(999)).rejects.toThrow('User not found');
});
// Using resolves matcher
test('should work with resolves matcher', async () => {
const mockUser = { id: 1, name: 'John' };
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUser,
});
await expect(userService.getUser(1)).resolves.toEqual(mockUser);
});
// Using rejects matcher
test('should work with rejects matcher', async () => {
fetch.mockResolvedValueOnce({
ok: false,
});
await expect(userService.getUser(999)).rejects.toThrow('User not found');
});
});
describe('createUser', () => {
test('should create user successfully', async () => {
const newUser = { name: 'New User', email: '[email protected]' };
const createdUser = { id: 2, ...newUser };
fetch.mockResolvedValueOnce({
ok: true,
json: async () => createdUser,
});
const result = await userService.createUser(newUser);
expect(fetch).toHaveBeenCalledWith('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newUser),
});
expect(result).toEqual(createdUser);
});
});
});
// 2. Testing with Timers
// src/utils/timer.js
export const timerUtils = {
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},
async waitForCondition(conditionFn, timeout = 5000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
if (await conditionFn()) {
return true;
}
await new Promise(resolve => setTimeout(resolve, 100));
}
throw new Error('Condition not met within timeout');
}
};
// src/utils/__tests__/timer.test.js
import { timerUtils } from '../timer';
describe('Timer Utils', () => {
// Use fake timers to control setTimeout
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
test('delay should resolve after specified time', async () => {
const delayPromise = timerUtils.delay(1000);
// Promise should not be resolved yet
let isResolved = false;
delayPromise.then(() => {
isResolved = true;
});
expect(isResolved).toBe(false);
// Fast-forward time
jest.advanceTimersByTime(1000);
// Wait for the promise to resolve
await Promise.resolve();
expect(isResolved).toBe(true);
});
test('waitForCondition should work with fake timers', async () => {
let conditionMet = false;
// Mock condition that becomes true after 2 seconds
const conditionFn = jest.fn(async () => {
await timerUtils.delay(100);
return conditionMet;
});
const waitForPromise = timerUtils.waitForCondition(conditionFn, 5000);
// Fast-forward 1 second - condition should still be false
jest.advanceTimersByTime(1100);
await Promise.resolve();
// Set condition to true and fast-forward more
conditionMet = true;
jest.advanceTimersByTime(200);
await Promise.resolve();
// Promise should now be resolved
await expect(waitForPromise).resolves.toBe(true);
});
});
// 3. Testing async callbacks and event handlers
// src/events/eventEmitter.js
export class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => {
// Simulate async callback
setTimeout(() => callback(data), 0);
});
}
}
}
// src/events/__tests__/eventEmitter.test.js
import { EventEmitter } from '../eventEmitter';
describe('EventEmitter - Async Callbacks', () => {
test('should execute async callbacks', async () => {
const emitter = new EventEmitter();
const callback = jest.fn();
emitter.on('test-event', callback);
emitter.emit('test-event', { message: 'hello' });
// Callback should not be called immediately due to setTimeout
expect(callback).not.toHaveBeenCalled();
// Wait for async callbacks to execute
await new Promise(resolve => setTimeout(resolve, 0));
expect(callback).toHaveBeenCalledWith({ message: 'hello' });
expect(callback).toHaveBeenCalledTimes(1);
});
test('should handle multiple async callbacks', async () => {
const emitter = new EventEmitter();
const callback1 = jest.fn();
const callback2 = jest.fn();
emitter.on('test-event', callback1);
emitter.on('test-event', callback2);
emitter.emit('test-event', { data: 'test' });
// Wait for all async callbacks
await new Promise(resolve => setTimeout(resolve, 0));
expect(callback1).toHaveBeenCalledWith({ data: 'test' });
expect(callback2).toHaveBeenCalledWith({ data: 'test' });
});
});
💻 Stratégies de Mocking et Stubbing javascript
Techniques avancées de mocking incluant mocking de modules, fonctions, APIs et implémentations mock personnalisées
// Advanced Mocking Strategies with Jest
// 1. Function Mocking
// src/utils/payment.js
export const paymentService = {
processPayment: async (amount, cardNumber) => {
// External API call
const response = await fetch('/api/payments', {
method: 'POST',
body: JSON.stringify({ amount, cardNumber })
});
return response.json();
},
validateCard: (cardNumber) => {
// Complex validation logic
return /^[0-9]{16}$/.test(cardNumber.replace(/\s/g, ''));
}
};
// src/utils/__tests__/payment.test.js
import { paymentService } from '../payment';
describe('Payment Service - Mocking Examples', () => {
beforeEach(() => {
// Reset all mocks before each test
jest.clearAllMocks();
});
test('should mock processPayment function', async () => {
// Mock the entire function
const mockProcessPayment = jest.spyOn(paymentService, 'processPayment');
mockProcessPayment.mockResolvedValue({
success: true,
transactionId: '12345'
});
const result = await paymentService.processPayment(100, '4111111111111111');
expect(mockProcessPayment).toHaveBeenCalledWith(100, '4111111111111111');
expect(result).toEqual({ success: true, transactionId: '12345' });
});
test('should mock card validation', () => {
const mockValidateCard = jest.spyOn(paymentService, 'validateCard');
mockValidateCard.mockReturnValue(true);
const isValid = paymentService.validateCard('invalid-card');
expect(mockValidateCard).toHaveBeenCalledWith('invalid-card');
expect(isValid).toBe(true);
});
});
// 2. Module Mocking
// src/services/api.js
import axios from 'axios';
export const apiService = {
async getUsers() {
const response = await axios.get('/api/users');
return response.data;
},
async createUser(userData) {
const response = await axios.post('/api/users', userData);
return response.data;
}
};
// src/services/__tests__/api.test.js
import axios from 'axios';
import { apiService } from '../api';
// Mock the entire axios module
jest.mock('axios');
describe('API Service - Module Mocking', () => {
test('should fetch users successfully', async () => {
const mockUsers = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
// Mock axios.get implementation
axios.get.mockResolvedValue({
data: mockUsers
});
const users = await apiService.getUsers();
expect(axios.get).toHaveBeenCalledWith('/api/users');
expect(users).toEqual(mockUsers);
});
test('should handle API errors', async () => {
const errorMessage = 'Network Error';
axios.get.mockRejectedValue(new Error(errorMessage));
await expect(apiService.getUsers()).rejects.toThrow(errorMessage);
});
test('should create user successfully', async () => {
const newUser = { name: 'New User', email: '[email protected]' };
const createdUser = { id: 3, ...newUser };
axios.post.mockResolvedValue({
data: createdUser
});
const result = await apiService.createUser(newUser);
expect(axios.post).toHaveBeenCalledWith('/api/users', newUser);
expect(result).toEqual(createdUser);
});
});
// 3. Mock Implementations
// src/utils/database.js
export const database = {
async connect() {
// Simulate database connection
await new Promise(resolve => setTimeout(resolve, 100));
return { connected: true };
},
async query(sql, params = []) {
// Simulate database query
console.log(`Executing: ${sql}`, params);
await new Promise(resolve => setTimeout(resolve, 50));
return { rows: [], rowCount: 0 };
}
};
// src/utils/__tests__/database.test.js
import { database } from '../database';
describe('Database - Mock Implementations', () => {
test('should mock database connection', async () => {
// Create a mock implementation
const mockConnect = jest.spyOn(database, 'connect');
mockConnect.mockImplementation(async () => {
console.log('Mock connection established');
return { connected: true, mock: true };
});
const result = await database.connect();
expect(mockConnect).toHaveBeenCalled();
expect(result).toEqual({ connected: true, mock: true });
});
test('should mock database query with custom implementation', async () => {
const mockQuery = jest.spyOn(database, 'query');
mockQuery.mockImplementation(async (sql, params) => {
console.log(`Mock query: ${sql}`, params);
// Return different results based on SQL
if (sql.includes('SELECT')) {
return {
rows: [{ id: 1, name: 'Mock User' }],
rowCount: 1
};
}
return { rows: [], rowCount: 0 };
});
const selectResult = await database.query('SELECT * FROM users');
const insertResult = await database.query('INSERT INTO users (name) VALUES (?)', ['Test']);
expect(mockQuery).toHaveBeenCalledTimes(2);
expect(selectResult.rows).toHaveLength(1);
expect(insertResult.rows).toHaveLength(0);
});
});
// 4. Manual Mocks
// __mocks__/localStorage.js
export const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
length: 0,
key: jest.fn(),
};
export default localStorageMock;
// src/utils/storage.js
export const storageService = {
saveUser(user) {
localStorage.setItem('user', JSON.stringify(user));
},
getUser() {
const userStr = localStorage.getItem('user');
return userStr ? JSON.parse(userStr) : null;
},
removeUser() {
localStorage.removeItem('user');
}
};
// src/utils/__tests__/storage.test.js
import localStorageMock from '../../__mocks__/localStorage';
// Mock the localStorage module
jest.mock('../localStorage', () => localStorageMock);
import { storageService } from '../storage';
describe('Storage Service - Manual Mocks', () => {
beforeEach(() => {
// Clear all mock calls before each test
localStorageMock.getItem.mockClear();
localStorageMock.setItem.mockClear();
localStorageMock.removeItem.mockClear();
});
test('should save user to localStorage', () => {
const user = { id: 1, name: 'John' };
storageService.saveUser(user);
expect(localStorageMock.setItem).toHaveBeenCalledWith(
'user',
JSON.stringify(user)
);
});
test('should get user from localStorage', () => {
const user = { id: 1, name: 'John' };
// Mock the return value
localStorageMock.getItem.mockReturnValue(JSON.stringify(user));
const result = storageService.getUser();
expect(localStorageMock.getItem).toHaveBeenCalledWith('user');
expect(result).toEqual(user);
});
test('should return null when no user in localStorage', () => {
localStorageMock.getItem.mockReturnValue(null);
const result = storageService.getUser();
expect(result).toBeNull();
});
test('should remove user from localStorage', () => {
storageService.removeUser();
expect(localStorageMock.removeItem).toHaveBeenCalledWith('user');
});
});
💻 Tests de Composants React javascript
Exemples complets de tests React utilisant Jest et React Testing Library
// React Component Testing with Jest and React Testing Library
// 1. Basic Component Testing
// src/components/Button.jsx
import React from 'react';
import PropTypes from 'prop-types';
const Button = ({ children, onClick, disabled = false, variant = 'primary' }) => {
const buttonClass = `button button--${variant}`;
return (
<button
className={buttonClass}
onClick={onClick}
disabled={disabled}
data-testid="button"
>
{children}
</button>
);
};
Button.propTypes = {
children: PropTypes.node.isRequired,
onClick: PropTypes.func,
disabled: PropTypes.bool,
variant: PropTypes.oneOf(['primary', 'secondary', 'danger'])
};
export default Button;
// src/components/__tests__/Button.test.jsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from '../Button';
describe('Button Component', () => {
test('renders with text content', () => {
render(<Button>Click me</Button>);
const button = screen.getByTestId('button');
expect(button).toBeInTheDocument();
expect(button).toHaveTextContent('Click me');
});
test('applies correct variant class', () => {
render(<Button variant="secondary">Secondary Button</Button>);
const button = screen.getByTestId('button');
expect(button).toHaveClass('button', 'button--secondary');
});
test('calls onClick handler when clicked', async () => {
const user = userEvent.setup();
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
const button = screen.getByTestId('button');
await user.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
test('does not call onClick when disabled', async () => {
const user = userEvent.setup();
const handleClick = jest.fn();
render(<Button onClick={handleClick} disabled>Disabled Button</Button>);
const button = screen.getByTestId('button');
expect(button).toBeDisabled();
await user.click(button);
expect(handleClick).not.toHaveBeenCalled();
});
test('has correct accessibility attributes', () => {
render(<Button disabled>Submit</Button>);
const button = screen.getByTestId('button');
expect(button).toHaveAttribute('disabled');
});
});
// 2. Complex Component Testing
// src/components/UserProfile.jsx
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('User not found');
}
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
if (userId) {
fetchUser();
}
}, [userId]);
if (loading) {
return <div data-testid="loading">Loading user profile...</div>;
}
if (error) {
return <div data-testid="error">Error: {error}</div>;
}
if (!user) {
return <div data-testid="no-user">No user selected</div>;
}
return (
<div data-testid="user-profile" className="user-profile">
<div className="user-avatar">
<img src={user.avatar} alt={`${user.name}'s avatar`} />
</div>
<div className="user-info">
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>{user.bio}</p>
</div>
</div>
);
};
UserProfile.propTypes = {
userId: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
};
export default UserProfile;
// src/components/__tests__/UserProfile.test.jsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import UserProfile from '../UserProfile';
// Mock fetch globally
global.fetch = jest.fn();
describe('UserProfile Component', () => {
beforeEach(() => {
fetch.mockClear();
});
test('shows loading state initially', () => {
fetch.mockImplementation(() => new Promise(() => {})); // Never resolves
render(<UserProfile userId="1" />);
expect(screen.getByTestId('loading')).toBeInTheDocument();
expect(screen.getByTestId('loading')).toHaveTextContent('Loading user profile...');
});
test('displays user data after successful fetch', async () => {
const mockUser = {
id: 1,
name: 'John Doe',
email: '[email protected]',
bio: 'Software developer',
avatar: 'https://example.com/avatar.jpg'
};
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUser,
});
render(<UserProfile userId="1" />);
// Wait for the user profile to appear
await waitFor(() => {
expect(screen.getByTestId('user-profile')).toBeInTheDocument();
});
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('[email protected]')).toBeInTheDocument();
expect(screen.getByText('Software developer')).toBeInTheDocument();
const avatar = screen.getByAltText("John Doe's avatar");
expect(avatar).toHaveAttribute('src', 'https://example.com/avatar.jpg');
});
test('displays error message when fetch fails', async () => {
fetch.mockResolvedValueOnce({
ok: false,
status: 404,
});
render(<UserProfile userId="999" />);
await waitFor(() => {
expect(screen.getByTestId('error')).toBeInTheDocument();
});
expect(screen.getByTestId('error')).toHaveTextContent('Error: User not found');
});
test('displays no user message when no userId provided', () => {
render(<UserProfile />);
expect(screen.getByTestId('no-user')).toBeInTheDocument();
expect(screen.getByTestId('no-user')).toHaveTextContent('No user selected');
expect(fetch).not.toHaveBeenCalled();
});
test('refetches user when userId changes', async () => {
const mockUser1 = { id: 1, name: 'User 1', email: '[email protected]' };
const mockUser2 = { id: 2, name: 'User 2', email: '[email protected]' };
fetch
.mockResolvedValueOnce({
ok: true,
json: async () => mockUser1,
})
.mockResolvedValueOnce({
ok: true,
json: async () => mockUser2,
});
const { rerender } = render(<UserProfile userId="1" />);
await waitFor(() => {
expect(screen.getByText('User 1')).toBeInTheDocument();
});
rerender(<UserProfile userId="2" />);
await waitFor(() => {
expect(screen.getByText('User 2')).toBeInTheDocument();
});
expect(fetch).toHaveBeenCalledTimes(2);
expect(fetch).toHaveBeenNthCalledWith(1, '/api/users/1');
expect(fetch).toHaveBeenNthCalledWith(2, '/api/users/2');
});
});
// 3. Custom Hook Testing
// src/hooks/useCounter.js
import { useState, useCallback } from 'react';
export const useCounter = (initialValue = 0) => {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => {
setCount(prev => prev + 1);
}, []);
const decrement = useCallback(() => {
setCount(prev => prev - 1);
}, []);
const reset = useCallback(() => {
setCount(initialValue);
}, [initialValue]);
return {
count,
increment,
decrement,
reset
};
};
// src/hooks/__tests__/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import { useCounter } from '../useCounter';
describe('useCounter Hook', () => {
test('should initialize with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
expect(typeof result.current.increment).toBe('function');
expect(typeof result.current.decrement).toBe('function');
expect(typeof result.current.reset).toBe('function');
});
test('should initialize with custom value', () => {
const { result } = renderHook(() => useCounter(5));
expect(result.current.count).toBe(5);
});
test('should increment count', () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('should decrement count', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(9);
});
test('should reset count to initial value', () => {
const { result } = renderHook(() => useCounter(7));
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(9);
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(7);
});
test('should handle multiple increments', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(3);
});
});
// 4. Integration Testing with Multiple Components
// src/components/UserForm.jsx
import React, { useState } from 'react';
import PropTypes from 'prop-types';
const UserForm = ({ onSubmit, initialUser = {} }) => {
const [user, setUser] = useState({
name: '',
email: '',
...initialUser
});
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(user);
};
const handleChange = (e) => {
const { name, value } = e.target;
setUser(prev => ({ ...prev, [name]: value }));
};
return (
<form data-testid="user-form" onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input
id="name"
name="name"
type="text"
value={user.name}
onChange={handleChange}
data-testid="name-input"
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
name="email"
type="email"
value={user.email}
onChange={handleChange}
data-testid="email-input"
/>
</div>
<button type="submit" data-testid="submit-button">Submit</button>
</form>
);
};
UserForm.propTypes = {
onSubmit: PropTypes.func.isRequired,
initialUser: PropTypes.object
};
export default UserForm;
// src/components/__tests__/UserForm.integration.test.jsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import UserForm from '../UserForm';
import UserProfile from '../UserProfile';
// Mock fetch for UserProfile
global.fetch = jest.fn();
describe('User Management Integration', () => {
beforeEach(() => {
fetch.mockClear();
});
test('should create user and display in profile', async () => {
const createdUser = {
id: 1,
name: 'John Doe',
email: '[email protected]',
bio: 'Software developer',
avatar: 'https://example.com/avatar.jpg'
};
// Mock the API calls
fetch
.mockResolvedValueOnce({
ok: true,
json: async () => createdUser,
})
.mockResolvedValueOnce({
ok: true,
json: async () => createdUser,
});
// Test component that combines form and profile
const UserManagement = () => {
const [currentUser, setCurrentUser] = React.useState(null);
const handleUserSubmit = async (userData) => {
// Simulate API call
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(userData)
});
const user = await response.json();
setCurrentUser(user);
};
return (
<div>
<UserForm onSubmit={handleUserSubmit} />
{currentUser && <UserProfile userId={currentUser.id} />}
</div>
);
};
render(<UserManagement />);
// Fill out the form
const nameInput = screen.getByTestId('name-input');
const emailInput = screen.getByTestId('email-input');
const submitButton = screen.getByTestId('submit-button');
await userEvent.type(nameInput, 'John Doe');
await userEvent.type(emailInput, '[email protected]');
await userEvent.click(submitButton);
// Wait for the user profile to appear
await waitFor(() => {
expect(screen.getByTestId('user-profile')).toBeInTheDocument();
});
// Verify the user data is displayed
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('[email protected]')).toBeInTheDocument();
});
});
💻 Développement Orienté par les Tests (TDD) javascript
Exemples complets de workflow TDD avec cycles red-green-refactor, organisation des tests et meilleures pratiques
// Test-Driven Development (TDD) with Jest
// 1. TDD Example: Building a String Calculator
// Test first, then implementation
// src/calculator/__tests__/stringCalculator.test.js
describe('String Calculator (TDD Example)', () => {
let stringCalculator;
beforeEach(() => {
// Import the implementation we're building
stringCalculator = require('../stringCalculator').default;
});
test('should return 0 for empty string', () => {
expect(stringCalculator.add("")).toBe(0);
});
test('should return number for single number string', () => {
expect(stringCalculator.add("1")).toBe(1);
expect(stringCalculator.add("5")).toBe(5);
});
test('should return sum for two numbers separated by comma', () => {
expect(stringCalculator.add("1,2")).toBe(3);
expect(stringCalculator.add("10,20")).toBe(30);
});
test('should return sum for multiple numbers', () => {
expect(stringCalculator.add("1,2,3,4,5")).toBe(15);
expect(stringCalculator.add("10,20,30")).toBe(60);
});
test('should handle new lines as delimiters', () => {
expect(stringCalculator.add("1\n2,3")).toBe(6);
expect(stringCalculator.add("4\n5\n6")).toBe(15);
});
test('should support custom delimiters', () => {
expect(stringCalculator.add("//;\n1;2;3")).toBe(6);
expect(stringCalculator.add("//|\n4|5|6")).toBe(15);
});
test('should throw error for negative numbers', () => {
expect(() => stringCalculator.add("1,-2,3")).toThrow("negative numbers not allowed: -2");
expect(() => stringCalculator.add("-1,2,-3")).toThrow("negative numbers not allowed: -1,-3");
});
test('should ignore numbers greater than 1000', () => {
expect(stringCalculator.add("2,1001,3")).toBe(5);
expect(stringCalculator.add("1000,1001,1002")).toBe(1000);
});
test('should support delimiters of any length', () => {
expect(stringCalculator.add("//[***]\n1***2***3")).toBe(6);
expect(stringCalculator.add("//[delimiter]\n4delimiter5delimiter6")).toBe(15);
});
test('should support multiple delimiters', () => {
expect(stringCalculator.add("//[*][%]\n1*2%3")).toBe(6);
expect(stringCalculator.add("//[**][%%]\n4**5%%6")).toBe(15);
});
});
// Implementation built through TDD
// src/calculator/stringCalculator.js
class StringCalculator {
add(numbers) {
if (!numbers) return 0;
let delimiter = /,|\n/;
let numbersString = numbers;
// Check for custom delimiters
if (numbers.startsWith("//")) {
const delimiterMatch = numbers.match(/^//(.+)\n(.+)$/);
if (delimiterMatch) {
const [, delimiterPart, numberString] = delimiterMatch;
// Handle multiple delimiters like [*][%]
const delimiterMatches = delimiterPart.match(/\[([^\]]+)\]/g);
if (delimiterMatches) {
const delimiters = delimiterMatches.map(match =>
match.slice(1, -1).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
);
delimiter = new RegExp(delimiters.join('|'));
} else {
// Single delimiter
const escapedDelimiter = delimiterPart.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
delimiter = new RegExp(escapedDelimiter);
}
numbersString = numberString;
}
}
const numberArray = numbersString.split(delimiter);
const validNumbers = numberArray
.map(num => parseInt(num.trim()))
.filter(num => !isNaN(num))
.filter(num => num <= 1000);
const negativeNumbers = validNumbers.filter(num => num < 0);
if (negativeNumbers.length > 0) {
throw new Error(`negative numbers not allowed: ${negativeNumbers.join(',')}`);
}
return validNumbers.reduce((sum, num) => sum + num, 0);
}
}
export default StringCalculator;
// 2. TDD Example: Building a Todo List Application
// src/todo/__tests__/todoManager.test.js
describe('Todo Manager (TDD Example)', () => {
let todoManager;
beforeEach(() => {
todoManager = require('../todoManager').default;
todoManager.clearTodos(); // Reset state for each test
});
describe('Adding todos', () => {
test('should add a new todo', () => {
const todo = todoManager.addTodo('Buy milk');
expect(todo).toMatchObject({
id: expect.any(Number),
text: 'Buy milk',
completed: false,
createdAt: expect.any(Date)
});
});
test('should throw error for empty todo text', () => {
expect(() => todoManager.addTodo('')).toThrow('Todo text cannot be empty');
expect(() => todoManager.addTodo(' ')).toThrow('Todo text cannot be empty');
});
test('should generate unique IDs', () => {
const todo1 = todoManager.addTodo('First task');
const todo2 = todoManager.addTodo('Second task');
expect(todo1.id).not.toBe(todo2.id);
});
});
describe('Retrieving todos', () => {
test('should return empty array initially', () => {
const todos = todoManager.getAllTodos();
expect(todos).toEqual([]);
});
test('should return all todos', () => {
todoManager.addTodo('Task 1');
todoManager.addTodo('Task 2');
const todos = todoManager.getAllTodos();
expect(todos).toHaveLength(2);
expect(todos[0].text).toBe('Task 1');
expect(todos[1].text).toBe('Task 2');
});
test('should return only active todos', () => {
todoManager.addTodo('Active task');
const completedTodo = todoManager.addTodo('Completed task');
todoManager.toggleTodo(completedTodo.id);
const activeTodos = todoManager.getActiveTodos();
expect(activeTodos).toHaveLength(1);
expect(activeTodos[0].text).toBe('Active task');
});
test('should return only completed todos', () => {
const activeTodo = todoManager.addTodo('Active task');
const completedTodo = todoManager.addTodo('Completed task');
todoManager.toggleTodo(completedTodo.id);
const completedTodos = todoManager.getCompletedTodos();
expect(completedTodos).toHaveLength(1);
expect(completedTodos[0].text).toBe('Completed task');
});
});
describe('Updating todos', () => {
test('should toggle todo completion', () => {
const todo = todoManager.addTodo('Test task');
expect(todo.completed).toBe(false);
const updatedTodo = todoManager.toggleTodo(todo.id);
expect(updatedTodo.completed).toBe(true);
expect(updatedTodo.completedAt).toBeInstanceOf(Date);
const toggledBack = todoManager.toggleTodo(todo.id);
expect(toggledBack.completed).toBe(false);
expect(toggledBack.completedAt).toBeNull();
});
test('should update todo text', () => {
const todo = todoManager.addTodo('Original text');
const updatedTodo = todoManager.updateTodoText(todo.id, 'Updated text');
expect(updatedTodo.text).toBe('Updated text');
expect(updatedTodo.updatedAt).toBeInstanceOf(Date);
});
test('should throw error when updating non-existent todo', () => {
expect(() => todoManager.toggleTodo(999)).toThrow('Todo not found');
expect(() => todoManager.updateTodoText(999, 'New text')).toThrow('Todo not found');
});
});
describe('Deleting todos', () => {
test('should delete todo', () => {
const todo = todoManager.addTodo('Task to delete');
expect(todoManager.getAllTodos()).toHaveLength(1);
todoManager.deleteTodo(todo.id);
expect(todoManager.getAllTodos()).toHaveLength(0);
});
test('should throw error when deleting non-existent todo', () => {
expect(() => todoManager.deleteTodo(999)).toThrow('Todo not found');
});
});
describe('Todo statistics', () => {
test('should return correct statistics', () => {
const todo1 = todoManager.addTodo('Task 1');
const todo2 = todoManager.addTodo('Task 2');
todoManager.toggleTodo(todo2.id);
const stats = todoManager.getStatistics();
expect(stats).toMatchObject({
total: 2,
active: 1,
completed: 1,
completionRate: 50
});
});
test('should handle empty statistics', () => {
const stats = todoManager.getStatistics();
expect(stats).toMatchObject({
total: 0,
active: 0,
completed: 0,
completionRate: 0
});
});
});
});
// Implementation built through TDD
// src/todo/todoManager.js
class TodoManager {
constructor() {
this.todos = [];
this.nextId = 1;
}
addTodo(text) {
if (!text || text.trim() === '') {
throw new Error('Todo text cannot be empty');
}
const todo = {
id: this.nextId++,
text: text.trim(),
completed: false,
createdAt: new Date(),
completedAt: null,
updatedAt: null
};
this.todos.push(todo);
return todo;
}
getAllTodos() {
return [...this.todos];
}
getActiveTodos() {
return this.todos.filter(todo => !todo.completed);
}
getCompletedTodos() {
return this.todos.filter(todo => todo.completed);
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (!todo) {
throw new Error('Todo not found');
}
todo.completed = !todo.completed;
todo.completedAt = todo.completed ? new Date() : null;
todo.updatedAt = new Date();
return todo;
}
updateTodoText(id, newText) {
if (!newText || newText.trim() === '') {
throw new Error('Todo text cannot be empty');
}
const todo = this.todos.find(t => t.id === id);
if (!todo) {
throw new Error('Todo not found');
}
todo.text = newText.trim();
todo.updatedAt = new Date();
return todo;
}
deleteTodo(id) {
const todoIndex = this.todos.findIndex(t => t.id === id);
if (todoIndex === -1) {
throw new Error('Todo not found');
}
this.todos.splice(todoIndex, 1);
}
getStatistics() {
const total = this.todos.length;
const completed = this.todos.filter(todo => todo.completed).length;
const active = total - completed;
const completionRate = total > 0 ? Math.round((completed / total) * 100) : 0;
return {
total,
active,
completed,
completionRate
};
}
clearTodos() {
this.todos = [];
this.nextId = 1;
}
}
const todoManager = new TodoManager();
export default todoManager;
// 3. Test Organization and Structure
// tests/setup.js
import { configure } from '@testing-library/react';
// Configure Testing Library
configure({ testIdAttribute: 'data-testid' });
// Global test utilities
global.testUtils = {
createMockUser: (overrides = {}) => ({
id: 1,
name: 'Test User',
email: '[email protected]',
...overrides
}),
createMockApiResponse: (data, options = {}) => ({
data,
status: 200,
statusText: 'OK',
headers: {},
config: {},
...options
}),
waitForAsync: () => new Promise(resolve => setTimeout(resolve, 0))
};
// Custom matchers
expect.extend({
toBeValidEmail(received) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const pass = emailRegex.test(received);
if (pass) {
return {
message: () => `expected ${received} not to be a valid email`,
pass: true,
};
} else {
return {
message: () => `expected ${received} to be a valid email`,
pass: false,
};
}
},
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
return {
message: () =>
pass
? `expected ${received} not to be within range ${floor} - ${ceiling}`
: `expected ${received} to be within range ${floor} - ${ceiling}`,
pass,
};
}
});
// 4. Best Practices and Patterns
// tests/utils/testUtils.js
export class TestBuilder {
constructor() {
this.setupFns = [];
this.teardownFns = [];
}
beforeEach(fn) {
this.setupFns.push(fn);
return this;
}
afterEach(fn) {
this.teardownFns.push(fn);
return this;
}
build(testFn) {
return async () => {
try {
// Run setup functions
for (const setupFn of this.setupFns) {
await setupFn();
}
// Run the actual test
await testFn();
} finally {
// Run teardown functions
for (const teardownFn of this.teardownFns) {
await teardownFn();
}
}
};
}
}
// Usage example:
const testBuilder = new TestBuilder()
.beforeEach(() => {
// Setup code
})
.afterEach(() => {
// Cleanup code
});
test('my test', testBuilder.build(() => {
// Test implementation
}));
// 5. Continuous Integration Testing Script
// scripts/test-ci.js
const { execSync } = require('child_process');
function runCommand(command, description) {
console.log(`🧪 ${description}...`);
try {
execSync(command, { stdio: 'inherit' });
console.log(`✅ ${description} - PASSED`);
return true;
} catch (error) {
console.log(`❌ ${description} - FAILED`);
return false;
}
}
async function runAllTests() {
const tests = [
{
command: 'jest --passWithNoTests --testPathPattern=__tests__',
description: 'Unit Tests'
},
{
command: 'jest --passWithNoTests --testPathPattern=integration',
description: 'Integration Tests'
},
{
command: 'jest --coverage --passWithNoTests',
description: 'Coverage Tests'
},
{
command: 'eslint src/ --ext .js,.jsx,.ts,.tsx',
description: 'Linting'
}
];
let allPassed = true;
for (const test of tests) {
const passed = runCommand(test.command, test.description);
if (!passed) {
allPassed = false;
// Continue running other tests for full feedback
}
}
if (allPassed) {
console.log('🎉 All tests passed!');
process.exit(0);
} else {
console.log('💥 Some tests failed!');
process.exit(1);
}
}
runAllTests().catch(console.error);