🎯 Exemplos recomendados
Balanced sample collections from various categories for you to explore
Framework de Testes Jest
Exemplos abrangentes de testes Jest incluindo testes unitários, de integração, mocking, testes assíncronos e padrões avançados para aplicações JavaScript/TypeScript
💻 Configuração Básica do Jest javascript
Configuração completa de projeto Jest com arquivos de configuração, estrutura básica de testes e utilitários comuns
// 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');
});
});
});
💻 Testes Assíncronos e de Promise javascript
Exemplos abrangentes de testes de código assíncrono, promises, async/await, timers e chamadas de 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' });
});
});
💻 Estratégias de Mocking e Stubbing javascript
Técnicas avançadas de mockery incluindo mockery de módulos, funções, APIs e implementações mock personalizadas
// 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');
});
});
💻 Testes de Componentes React javascript
Exemplos completos de testes React usando Jest e 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();
});
});
💻 Desenvolvimento Orientado a Testes (TDD) javascript
Exemplos completos de fluxo de trabalho TDD com ciclos red-green-refactor, organização de testes e melhores práticas
// 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);