Jest Cheatsheet¶
¶
¶
■h1 títuloJest - Testing de JavaScript encantador "Clase de inscripción" Jest es un marco de prueba de JavaScript encantador con un enfoque en la simplicidad. Funciona fuera de la caja para la mayoría de proyectos de JavaScript y proporciona características como pruebas de instantáneas, corredor de prueba incorporado, biblioteca de aserción y poderosas capacidades de burla. ▪/p] ■/div titulada
¶
########################################################################################################################################################################################################################################################## Copiar todos los comandos¶
########################################################################################################################################################################################################################################################## Generar PDF seleccionado/button¶
■/div titulada ■/div titulada
Cuadro de contenidos¶
- Instalación
- Empezar
- Basic Testing
- Matchers
- Async Testing
- Mocking
- Snapshot Testing
- Configuración
- React Testing
- Code Coverage
- Características avanzadas
- Testing Patterns
- Performance
- Integración
- Solucionando
- Las mejores prácticas
Instalación¶
Instalación básica¶
# Install Jest
npm install --save-dev jest
# Install with TypeScript support
npm install --save-dev jest @types/jest ts-jest
# Install with Babel support
npm install --save-dev jest babel-jest @babel/core @babel/preset-env
# Global installation (not recommended)
npm install -g jest
Proyectos de reforma¶
# Create React App (Jest included)
npx create-react-app my-app
cd my-app
# Manual React setup
npm install --save-dev jest @testing-library/react @testing-library/jest-dom
# React Native
npm install --save-dev jest react-test-renderer
Paquete.json Configuración¶
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --ci --coverage --watchAll=false"
},
"devDependencies": {
"jest": "^29.0.0",
"@testing-library/jest-dom": "^5.16.0"
}
}
Estructura del proyecto¶
my-project/
├── src/
│ ├── components/
│ │ ├── Button.js
│ │ └── Button.test.js
│ ├── utils/
│ │ ├── math.js
│ │ └── math.test.js
│ └── __tests__/
│ └── integration.test.js
├── __mocks__/
│ └── fileMock.js
├── jest.config.js
└── package.json
Comienzo¶
Primera prueba¶
// math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export function multiply(a, b) {
return a * b;
}
export function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
// math.test.js
import { add, subtract, multiply, divide } from './math';
describe('Math functions', () => {
test('adds 1 + 2 to equal 3', () => {
expect(add(1, 2)).toBe(3);
});
test('subtracts 5 - 3 to equal 2', () => {
expect(subtract(5, 3)).toBe(2);
});
test('multiplies 3 * 4 to equal 12', () => {
expect(multiply(3, 4)).toBe(12);
});
test('divides 8 / 2 to equal 4', () => {
expect(divide(8, 2)).toBe(4);
});
test('throws error when dividing by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
Pruebas de ejecución¶
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run specific test file
npm test math.test.js
# Run tests matching pattern
npm test -- --testNamePattern="add"
# Run tests with coverage
npm run test:coverage
# Run tests in CI mode
npm run test:ci
Organización de los ensayos¶
// Grouping tests with describe
describe('Calculator', () => {
describe('Addition', () => {
test('should add positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('should add negative numbers', () => {
expect(add(-2, -3)).toBe(-5);
});
test('should add zero', () => {
expect(add(5, 0)).toBe(5);
});
});
describe('Division', () => {
test('should divide positive numbers', () => {
expect(divide(10, 2)).toBe(5);
});
test('should handle division by zero', () => {
expect(() => divide(10, 0)).toThrow();
});
});
});
Pruebas básicas¶
Estructura de ensayo¶
// Basic test structure
test('description of what is being tested', () => {
// Arrange - set up test data
const input = 'hello';
const expected = 'HELLO';
// Act - execute the function
const result = input.toUpperCase();
// Assert - verify the result
expect(result).toBe(expected);
});
// Alternative syntax
it('should convert string to uppercase', () => {
expect('hello'.toUpperCase()).toBe('HELLO');
});
Configuración y Teardown¶
describe('Database tests', () => {
let database;
// Run before all tests in this describe block
beforeAll(async () => {
database = await connectToDatabase();
});
// Run after all tests in this describe block
afterAll(async () => {
await database.close();
});
// Run before each test
beforeEach(() => {
database.clear();
});
// Run after each test
afterEach(() => {
database.cleanup();
});
test('should save user', async () => {
const user = { name: 'John', email: 'john@example.com' };
await database.save(user);
const savedUser = await database.findByEmail('john@example.com');
expect(savedUser).toEqual(user);
});
});
Pruebas de Skipping y Focusing¶
// Skip tests
describe.skip('Skipped test suite', () => {
test('this will not run', () => {
expect(true).toBe(false);
});
});
test.skip('skipped test', () => {
expect(true).toBe(false);
});
// Focus on specific tests (only these will run)
describe.only('Focused test suite', () => {
test('this will run', () => {
expect(true).toBe(true);
});
});
test.only('focused test', () => {
expect(true).toBe(true);
});
// Conditional tests
const runIntegrationTests = process.env.NODE_ENV === 'test';
(runIntegrationTests ? describe : describe.skip)('Integration tests', () => {
test('should integrate with external service', () => {
// Integration test code
});
});
Pruebas parametrizadas¶
// Test with multiple inputs
describe('isPrime function', () => {
test.each([
[2, true],
[3, true],
[4, false],
[5, true],
[6, false],
[7, true],
[8, false],
[9, false],
[10, false],
[11, true],
])('isPrime(%i) should return %s', (input, expected) => {
expect(isPrime(input)).toBe(expected);
});
});
// Test with objects
describe('user validation', () => {
test.each([
{ name: 'John', email: 'john@example.com', valid: true },
{ name: '', email: 'john@example.com', valid: false },
{ name: 'John', email: 'invalid-email', valid: false },
{ name: 'John', email: '', valid: false },
])('validateUser($name, $email) should return $valid', ({ name, email, valid }) => {
expect(validateUser({ name, email })).toBe(valid);
});
});
Partidos¶
Partidos básicos¶
describe('Basic matchers', () => {
test('equality matchers', () => {
expect(2 + 2).toBe(4); // Exact equality (Object.is)
expect({ name: 'John' }).toEqual({ name: 'John' }); // Deep equality
expect({ name: 'John' }).not.toBe({ name: 'John' }); // Different objects
});
test('truthiness matchers', () => {
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('hello').toBeDefined();
});
test('number matchers', () => {
expect(2 + 2).toBeGreaterThan(3);
expect(2 + 2).toBeGreaterThanOrEqual(4);
expect(2 + 2).toBeLessThan(5);
expect(2 + 2).toBeLessThanOrEqual(4);
expect(0.1 + 0.2).toBeCloseTo(0.3); // Floating point
});
});
String Matchers¶
describe('String matchers', () => {
test('string matching', () => {
expect('Hello World').toMatch(/World/);
expect('Hello World').toMatch('World');
expect('user@example.com').toMatch(/^[\w\.-]+@[\w\.-]+\.\w+$/);
expect('Hello World').toContain('World');
expect('Hello World').not.toContain('Goodbye');
expect('Hello World').toHaveLength(11);
expect('').toHaveLength(0);
});
});
Array and Object Matchers¶
describe('Array and object matchers', () => {
test('array matchers', () => {
const fruits = ['apple', 'banana', 'orange'];
expect(fruits).toContain('banana');
expect(fruits).toHaveLength(3);
expect(fruits).toEqual(['apple', 'banana', 'orange']);
expect(fruits).toEqual(expect.arrayContaining(['apple', 'banana']));
});
test('object matchers', () => {
const user = {
id: 1,
name: 'John',
email: 'john@example.com',
profile: {
age: 30,
city: 'New York'
}
};
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('profile.age', 30);
expect(user).toMatchObject({
name: 'John',
email: 'john@example.com'
});
expect(user).toEqual(expect.objectContaining({
name: 'John',
id: expect.any(Number)
}));
});
});
Exception Matchers¶
describe('Exception matchers', () => {
test('function throws', () => {
const throwError = () => {
throw new Error('Something went wrong');
};
expect(throwError).toThrow();
expect(throwError).toThrow('Something went wrong');
expect(throwError).toThrow(/wrong/);
expect(throwError).toThrow(Error);
});
test('async function throws', async () => {
const asyncThrowError = async () => {
throw new Error('Async error');
};
await expect(asyncThrowError()).rejects.toThrow('Async error');
});
});
Partidos aduaneros¶
// Custom matcher
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,
};
}
},
});
// Usage
test('numeric ranges', () => {
expect(100).toBeWithinRange(90, 110);
expect(101).not.toBeWithinRange(0, 100);
});
// Async custom matcher
expect.extend({
async toBeValidUser(received) {
const isValid = await validateUserAsync(received);
if (isValid) {
return {
message: () => `expected ${received} not to be a valid user`,
pass: true,
};
} else {
return {
message: () => `expected ${received} to be a valid user`,
pass: false,
};
}
},
});
Pruebas Async¶
Promesas¶
describe('Promise testing', () => {
// Return promise
test('async function returns promise', () => {
return fetchUser(1).then(user => {
expect(user.name).toBe('John');
});
});
// Async/await
test('async function with async/await', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('John');
});
// Testing rejections
test('async function rejects', async () => {
await expect(fetchUser(-1)).rejects.toThrow('User not found');
});
// Testing both resolve and reject
test('promise resolves', () => {
return expect(fetchUser(1)).resolves.toEqual({
id: 1,
name: 'John'
});
});
});
Callbacks¶
describe('Callback testing', () => {
// Callback with done
test('callback function', (done) => {
function callback(data) {
try {
expect(data).toBe('callback data');
done();
} catch (error) {
done(error);
}
}
fetchDataCallback(callback);
});
// Testing error callbacks
test('error callback', (done) => {
function errorCallback(error) {
try {
expect(error.message).toBe('Something went wrong');
done();
} catch (err) {
done(err);
}
}
fetchDataWithError(errorCallback);
});
});
Timers and Delays¶
describe('Timer testing', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
test('delayed function', () => {
const callback = jest.fn();
setTimeout(callback, 1000);
// Fast-forward time
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
});
test('interval function', () => {
const callback = jest.fn();
setInterval(callback, 1000);
// Fast-forward multiple intervals
jest.advanceTimersByTime(3000);
expect(callback).toHaveBeenCalledTimes(3);
});
test('run all timers', () => {
const callback = jest.fn();
setTimeout(callback, 1000);
setTimeout(callback, 2000);
jest.runAllTimers();
expect(callback).toHaveBeenCalledTimes(2);
});
});
Mocking¶
Función Mocking¶
describe('Function mocking', () => {
test('mock function', () => {
const mockFn = jest.fn();
mockFn('arg1', 'arg2');
mockFn('arg3');
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenLastCalledWith('arg3');
});
test('mock return values', () => {
const mockFn = jest.fn();
mockFn.mockReturnValue(42);
expect(mockFn()).toBe(42);
mockFn.mockReturnValueOnce(1).mockReturnValueOnce(2);
expect(mockFn()).toBe(1);
expect(mockFn()).toBe(2);
expect(mockFn()).toBe(42); // Default return value
});
test('mock implementation', () => {
const mockFn = jest.fn((x, y) => x + y);
expect(mockFn(1, 2)).toBe(3);
expect(mockFn).toHaveBeenCalledWith(1, 2);
});
});
Módulo Mocking¶
// userService.js
import axios from 'axios';
export async function getUser(id) {
const response = await axios.get(`/api/users/${id}`);
return response.data;
}
export async function createUser(userData) {
const response = await axios.post('/api/users', userData);
return response.data;
}
// userService.test.js
import axios from 'axios';
import { getUser, createUser } from './userService';
// Mock the entire axios module
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe('User Service', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('should fetch user', async () => {
const userData = { id: 1, name: 'John' };
mockedAxios.get.mockResolvedValue({ data: userData });
const user = await getUser(1);
expect(user).toEqual(userData);
expect(mockedAxios.get).toHaveBeenCalledWith('/api/users/1');
});
test('should create user', async () => {
const newUser = { name: 'Jane', email: 'jane@example.com' };
const createdUser = { id: 2, ...newUser };
mockedAxios.post.mockResolvedValue({ data: createdUser });
const user = await createUser(newUser);
expect(user).toEqual(createdUser);
expect(mockedAxios.post).toHaveBeenCalledWith('/api/users', newUser);
});
});
Mocking parcial¶
// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
// calculator.js
import * as math from './math';
export function calculate(operation, a, b) {
switch (operation) {
case 'add':
return math.add(a, b);
case 'subtract':
return math.subtract(a, b);
case 'multiply':
return math.multiply(a, b);
default:
throw new Error('Unknown operation');
}
}
// calculator.test.js
import { calculate } from './calculator';
import * as math from './math';
// Partial mock - only mock specific functions
jest.mock('./math', () => ({
...jest.requireActual('./math'),
add: jest.fn(),
}));
const mockedMath = math as jest.Mocked<typeof math>;
describe('Calculator', () => {
test('should use mocked add function', () => {
mockedMath.add.mockReturnValue(100);
const result = calculate('add', 2, 3);
expect(result).toBe(100);
expect(mockedMath.add).toHaveBeenCalledWith(2, 3);
});
test('should use real subtract function', () => {
const result = calculate('subtract', 5, 3);
expect(result).toBe(2); // Real implementation
});
});
Funciones de espía¶
describe('Spy functions', () => {
test('spy on object method', () => {
const user = {
getName: () => 'John',
setName: (name) => { this.name = name; }
};
const spy = jest.spyOn(user, 'getName');
const name = user.getName();
expect(spy).toHaveBeenCalled();
expect(name).toBe('John');
spy.mockRestore(); // Restore original implementation
});
test('spy on console.log', () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
console.log('Hello, World!');
expect(consoleSpy).toHaveBeenCalledWith('Hello, World!');
consoleSpy.mockRestore();
});
});
Mock Files¶
// __mocks__/fs.js
const fs = jest.createMockFromModule('fs');
let mockFiles = Object.create(null);
function __setMockFiles(newMockFiles) {
mockFiles = Object.create(null);
for (const file in newMockFiles) {
const dir = path.dirname(file);
if (!mockFiles[dir]) {
mockFiles[dir] = [];
}
mockFiles[dir].push(path.basename(file));
}
}
function readFileSync(filePath) {
return mockFiles[filePath] || '';
}
fs.__setMockFiles = __setMockFiles;
fs.readFileSync = readFileSync;
module.exports = fs;
// fileReader.test.js
import fs from 'fs';
import { readConfigFile } from './fileReader';
jest.mock('fs');
describe('File Reader', () => {
test('should read config file', () => {
fs.__setMockFiles({
'/config/app.json': '{"name": "MyApp"}'
});
const config = readConfigFile('/config/app.json');
expect(config).toEqual({ name: 'MyApp' });
});
});
Pruebas de instantáneas¶
Corridas básicas¶
// Button.js
import React from 'react';
export function Button({ children, onClick, variant = 'primary' }) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
>
{children}
</button>
);
}
// Button.test.js
import React from 'react';
import { render } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
test('should match snapshot', () => {
const { container } = render(
<Button variant="primary">Click me</Button>
);
expect(container.firstChild).toMatchSnapshot();
});
test('should match snapshot with different variant', () => {
const { container } = render(
<Button variant="secondary">Cancel</Button>
);
expect(container.firstChild).toMatchSnapshot();
});
});
Inline Snapshots¶
describe('Inline snapshots', () => {
test('should match inline snapshot', () => {
const user = {
id: 1,
name: 'John',
createdAt: new Date('2023-01-01')
};
expect(user).toMatchInlineSnapshot(`
Object {
"createdAt": 2023-01-01T00:00:00.000Z,
"id": 1,
"name": "John",
}
`);
});
});
Serializadores de instantánea personalizados¶
// Custom serializer for Date objects
expect.addSnapshotSerializer({
test: (val) => val instanceof Date,
print: (val) => `Date("${val.toISOString()}")`,
});
// Custom serializer for React elements
expect.addSnapshotSerializer({
test: (val) => val && val.$$typeof === Symbol.for('react.element'),
print: (val, serialize) => {
const { type, props } = val;
const { children, ...restProps } = props;
return `<${type}${Object.keys(restProps).length ? ' ' + serialize(restProps) : ''}>${
children ? serialize(children) : ''
}</${type}>`;
},
});
Partidos de propiedad¶
describe('Property matchers', () => {
test('should match snapshot with dynamic values', () => {
const user = {
id: Math.random(),
name: 'John',
createdAt: new Date(),
profile: {
lastLogin: new Date(),
sessionId: 'abc123'
}
};
expect(user).toMatchSnapshot({
id: expect.any(Number),
createdAt: expect.any(Date),
profile: {
lastLogin: expect.any(Date),
sessionId: expect.any(String)
}
});
});
});
Configuración¶
Configuración Jest Archivo¶
// jest.config.js
module.exports = {
// Test environment
testEnvironment: 'jsdom', // 'node' | 'jsdom'
// Test file patterns
testMatch: [
'**/__tests__/**/*.(js|jsx|ts|tsx)',
'**/*.(test|spec).(js|jsx|ts|tsx)'
],
// Setup files
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
// Module name mapping
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)$': '<rootDir>/__mocks__/fileMock.js'
},
// Transform files
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
'^.+\\.css$': '<rootDir>/config/jest/cssTransform.js'
},
// Coverage
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.js',
'!src/serviceWorker.js'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
// Ignore patterns
testPathIgnorePatterns: [
'/node_modules/',
'/build/',
'/dist/'
],
// Module directories
moduleDirectories: ['node_modules', '<rootDir>/src'],
// Globals
globals: {
'ts-jest': {
tsconfig: 'tsconfig.json'
}
}
};
Configuración TipoScript¶
// jest.config.js for TypeScript
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1'
},
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest'
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
globals: {
'ts-jest': {
tsconfig: {
jsx: 'react-jsx'
}
}
}
};
Configuración de archivos¶
// src/setupTests.js
import '@testing-library/jest-dom';
// Mock IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
constructor() {}
disconnect() {}
observe() {}
unobserve() {}
};
// Mock matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
// Global test utilities
global.testUtils = {
createMockUser: () => ({
id: 1,
name: 'Test User',
email: 'test@example.com'
})
};
Medio ambiente¶
// jest.config.js
module.exports = {
setupFiles: ['<rootDir>/config/jest/setEnvVars.js'],
};
// config/jest/setEnvVars.js
process.env.NODE_ENV = 'test';
process.env.API_URL = 'http://localhost:3001';
process.env.FEATURE_FLAG_NEW_UI = 'true';
Pruebas de reacción¶
Pruebas de componentes¶
// UserProfile.js
import React, { useState, useEffect } from 'react';
export function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUser() {
try {
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);
}
}
fetchUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
<p>Age: {user.age}</p>
</div>
);
}
// UserProfile.test.js
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { UserProfile } from './UserProfile';
// Mock fetch
global.fetch = jest.fn();
describe('UserProfile', () => {
beforeEach(() => {
fetch.mockClear();
});
test('should display loading state', () => {
fetch.mockImplementation(() => new Promise(() => {})); // Never resolves
render(<UserProfile userId={1} />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
test('should display user data', async () => {
const mockUser = {
id: 1,
name: 'John Doe',
email: 'john@example.com',
age: 30
};
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUser
});
render(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
expect(screen.getByText('Email: john@example.com')).toBeInTheDocument();
expect(screen.getByText('Age: 30')).toBeInTheDocument();
});
test('should display error message', async () => {
fetch.mockRejectedValueOnce(new Error('Network error'));
render(<UserProfile userId={1} />);
await waitFor(() => {
expect(screen.getByText('Error: Network error')).toBeInTheDocument();
});
});
});
Pruebas de eventos¶
// Counter.js
import React, { useState } from 'react';
export function Counter({ initialValue = 0, onCountChange }) {
const [count, setCount] = useState(initialValue);
const increment = () => {
const newCount = count + 1;
setCount(newCount);
onCountChange?.(newCount);
};
const decrement = () => {
const newCount = count - 1;
setCount(newCount);
onCountChange?.(newCount);
};
const reset = () => {
setCount(initialValue);
onCountChange?.(initialValue);
};
return (
<div>
<span data-testid="count">{count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}
// Counter.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
describe('Counter', () => {
test('should increment count', async () => {
const user = userEvent.setup();
render(<Counter />);
const incrementButton = screen.getByText('+');
await user.click(incrementButton);
expect(screen.getByTestId('count')).toHaveTextContent('1');
});
test('should call onCountChange when count changes', async () => {
const user = userEvent.setup();
const onCountChange = jest.fn();
render(<Counter onCountChange={onCountChange} />);
const incrementButton = screen.getByText('+');
await user.click(incrementButton);
expect(onCountChange).toHaveBeenCalledWith(1);
});
test('should reset to initial value', async () => {
const user = userEvent.setup();
render(<Counter initialValue={5} />);
const incrementButton = screen.getByText('+');
const resetButton = screen.getByText('Reset');
await user.click(incrementButton);
expect(screen.getByTestId('count')).toHaveTextContent('6');
await user.click(resetButton);
expect(screen.getByTestId('count')).toHaveTextContent('5');
});
});
Pruebas de gancho¶
// useCounter.js
import { useState, useCallback } from 'react';
export function 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 };
}
// useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
test('should initialize with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
test('should initialize with custom value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
test('should increment count', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('should decrement count', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
test('should reset count', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(12);
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(10);
});
});
Cobertura del Código¶
Configuración de cobertura¶
// jest.config.js
module.exports = {
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/index.js',
'!src/reportWebVitals.js',
'!src/**/*.stories.{js,jsx,ts,tsx}',
'!src/**/*.test.{js,jsx,ts,tsx}'
],
coverageDirectory: 'coverage',
coverageReporters: [
'text',
'lcov',
'html',
'json'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
},
'./src/components/': {
branches: 90,
functions: 90,
lines: 90,
statements: 90
}
}
};
Scripts de cobertura¶
{
"scripts": {
"test:coverage": "jest --coverage",
"test:coverage:watch": "jest --coverage --watchAll",
"test:coverage:ci": "jest --coverage --ci --watchAll=false",
"coverage:open": "open coverage/lcov-report/index.html"
}
}
Ignorar la cobertura¶
// Ignore specific lines
function myFunction() {
/* istanbul ignore next */
if (process.env.NODE_ENV === 'development') {
console.log('Development mode');
}
return 'result';
}
// Ignore entire function
/* istanbul ignore next */
function debugFunction() {
console.log('Debug info');
}
// Ignore else branch
function processValue(value) {
if (value) {
return value.toUpperCase();
}
/* istanbul ignore else */
else {
return '';
}
}
Características avanzadas¶
Custom Test Environment¶
// jest-environment-custom.js
const { TestEnvironment } = require('jest-environment-jsdom');
class CustomTestEnvironment extends TestEnvironment {
constructor(config, context) {
super(config, context);
// Add custom globals
this.global.customGlobal = 'test-value';
}
async setup() {
await super.setup();
// Custom setup logic
this.global.mockDatabase = {
users: [],
addUser: (user) => this.global.mockDatabase.users.push(user),
getUser: (id) => this.global.mockDatabase.users.find(u => u.id === id)
};
}
async teardown() {
// Custom teardown logic
this.global.mockDatabase = null;
await super.teardown();
}
}
module.exports = CustomTestEnvironment;
// jest.config.js
module.exports = {
testEnvironment: './jest-environment-custom.js'
};
Custom Reporters¶
// custom-reporter.js
class CustomReporter {
constructor(globalConfig, options) {
this._globalConfig = globalConfig;
this._options = options;
}
onRunStart(results, options) {
console.log('🚀 Starting test run...');
}
onTestStart(test) {
console.log(`▶️ Running ${test.path}`);
}
onTestResult(test, testResult, aggregatedResult) {
if (testResult.numFailingTests > 0) {
console.log(`❌ ${test.path} - ${testResult.numFailingTests} failed`);
} else {
console.log(`✅ ${test.path} - all tests passed`);
}
}
onRunComplete(contexts, results) {
console.log(`🏁 Test run complete: ${results.numPassedTests} passed, ${results.numFailedTests} failed`);
}
}
module.exports = CustomReporter;
// jest.config.js
module.exports = {
reporters: [
'default',
['./custom-reporter.js', { option1: 'value1' }]
]
};
Global Setup and Teardown¶
// jest.config.js
module.exports = {
globalSetup: './config/jest/globalSetup.js',
globalTeardown: './config/jest/globalTeardown.js'
};
// config/jest/globalSetup.js
module.exports = async () => {
console.log('🔧 Global setup');
// Start test database
global.__TEST_DB__ = await startTestDatabase();
// Start mock server
global.__MOCK_SERVER__ = await startMockServer();
};
// config/jest/globalTeardown.js
module.exports = async () => {
console.log('🧹 Global teardown');
// Stop test database
await global.__TEST_DB__.stop();
// Stop mock server
await global.__MOCK_SERVER__.stop();
};
Ver Plugins¶
// watch-plugin-custom.js
class CustomWatchPlugin {
constructor({ stdin, stdout, config, testPathPattern }) {
this._stdin = stdin;
this._stdout = stdout;
this._config = config;
this._testPathPattern = testPathPattern;
}
apply(jestHooks) {
jestHooks.onFileChange(({ projects }) => {
console.log('📁 Files changed, running tests...');
});
}
getUsageInfo() {
return {
key: 'c',
prompt: 'clear console'
};
}
run(globalConfig, updateConfigAndRun) {
console.clear();
return Promise.resolve();
}
}
module.exports = CustomWatchPlugin;
// jest.config.js
module.exports = {
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname',
'./watch-plugin-custom.js'
]
};
```_
## Patrones de prueba
### Page Object Model
```javascript
// pageObjects/LoginPage.js
export class LoginPage {
constructor(page) {
this.page = page;
}
get emailInput() {
return this.page.getByLabelText(/email/i);
}
get passwordInput() {
return this.page.getByLabelText(/password/i);
}
get submitButton() {
return this.page.getByRole('button', { name: /login/i });
}
get errorMessage() {
return this.page.getByTestId('error-message');
}
async login(email, password) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectErrorMessage(message) {
await expect(this.errorMessage).toHaveText(message);
}
}
// Login.test.js
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginPage } from './pageObjects/LoginPage';
import { Login } from './Login';
describe('Login', () => {
test('should display error for invalid credentials', async () => {
const user = userEvent.setup();
render(<Login />);
const loginPage = new LoginPage(screen);
await loginPage.login('invalid@email.com', 'wrongpassword');
await loginPage.expectErrorMessage('Invalid credentials');
});
});
Factores de prueba¶
// factories/userFactory.js
export const createUser = (overrides = {}) => ({
id: Math.floor(Math.random() * 1000),
name: 'John Doe',
email: 'john@example.com',
age: 30,
isActive: true,
createdAt: new Date(),
...overrides
});
export const createUsers = (count, overrides = {}) =>
Array.from({ length: count }, (_, index) =>
createUser({ id: index + 1, ...overrides })
);
// Usage in tests
import { createUser, createUsers } from './factories/userFactory';
describe('User Service', () => {
test('should process user data', () => {
const user = createUser({ name: 'Jane Doe', age: 25 });
const result = processUser(user);
expect(result.displayName).toBe('Jane Doe (25)');
});
test('should handle multiple users', () => {
const users = createUsers(5, { isActive: true });
const activeUsers = filterActiveUsers(users);
expect(activeUsers).toHaveLength(5);
});
});
Test Utilities¶
// testUtils/renderWithProviders.js
import React from 'react';
import { render } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ThemeProvider } from 'styled-components';
import { theme } from '../src/theme';
export function renderWithProviders(
ui,
{
initialEntries = ['/'],
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false }
}
}),
...renderOptions
} = {}
) {
function Wrapper({ children }) {
return (
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={theme}>
{children}
</ThemeProvider>
</QueryClientProvider>
</BrowserRouter>
);
}
return render(ui, { wrapper: Wrapper, ...renderOptions });
}
// Usage
import { renderWithProviders } from './testUtils/renderWithProviders';
test('should render with all providers', () => {
renderWithProviders(<MyComponent />);
// Test implementation
});
Matchers personalizados para Domain Logic¶
// matchers/userMatchers.js
expect.extend({
toBeValidUser(received) {
const pass =
received &&
typeof received.id === 'number' &&
typeof received.name === 'string' &&
received.name.length > 0 &&
typeof received.email === 'string' &&
received.email.includes('@');
if (pass) {
return {
message: () => `expected ${JSON.stringify(received)} not to be a valid user`,
pass: true,
};
} else {
return {
message: () => `expected ${JSON.stringify(received)} to be a valid user`,
pass: false,
};
}
},
toHavePermission(received, permission) {
const pass = received.permissions && received.permissions.includes(permission);
if (pass) {
return {
message: () => `expected user not to have permission "${permission}"`,
pass: true,
};
} else {
return {
message: () => `expected user to have permission "${permission}"`,
pass: false,
};
}
}
});
// Usage
test('should validate user object', () => {
const user = { id: 1, name: 'John', email: 'john@example.com' };
expect(user).toBeValidUser();
});
test('should check user permissions', () => {
const user = { permissions: ['read', 'write'] };
expect(user).toHavePermission('read');
});
Ejecución¶
Optimización del rendimiento de prueba¶
// jest.config.js
module.exports = {
// Parallel execution
maxWorkers: '50%', // Use 50% of available cores
// Cache
cache: true,
cacheDirectory: '/tmp/jest_cache',
// Faster test discovery
testPathIgnorePatterns: [
'/node_modules/',
'/build/',
'/coverage/'
],
// Optimize transforms
transform: {
'^.+\\.(js|jsx)$': ['babel-jest', { cacheDirectory: true }]
},
// Reduce overhead
clearMocks: true,
restoreMocks: true,
// Faster test environment
testEnvironment: 'jsdom',
testEnvironmentOptions: {
url: 'http://localhost'
}
};
```_
### Prueba selectiva Corrección
```bash
# Run only changed files
npm test -- --onlyChanged
# Run related tests
npm test -- --findRelatedTests src/components/Button.js
# Run tests matching pattern
npm test -- --testNamePattern="should render"
# Run specific test suites
npm test -- --testPathPattern="components"
# Bail on first failure
npm test -- --bail
# Run tests in band (no parallel)
npm test -- --runInBand
Optimización de memoria¶
// jest.config.js
module.exports = {
// Limit memory usage
workerIdleMemoryLimit: '512MB',
// Clear modules between tests
clearMocks: true,
resetMocks: true,
restoreMocks: true,
// Optimize setup
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
// Reduce test overhead
testEnvironment: 'jsdom',
// Optimize coverage collection
collectCoverageFrom: [
'src/**/*.{js,jsx}',
'!src/**/*.test.{js,jsx}',
'!src/**/*.stories.{js,jsx}'
]
};
// Cleanup in tests
afterEach(() => {
jest.clearAllMocks();
cleanup(); // From @testing-library/react
});
Integración¶
CI/CD Integration¶
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test -- --ci --coverage --watchAll=false
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
Docker Integration¶
# Dockerfile.test
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npm", "test", "--", "--ci", "--coverage", "--watchAll=false"]
IDE Integration¶
// .vscode/settings.json
{
"jest.jestCommandLine": "npm test --",
"jest.autoRun": {
"watch": true,
"onStartup": ["all-tests"]
},
"jest.showCoverageOnLoad": true,
"jest.coverageFormatter": "DefaultFormatter",
"jest.debugMode": true
}
Solución de problemas¶
Cuestiones comunes¶
# Clear Jest cache
npx jest --clearCache
# Debug Jest configuration
npx jest --showConfig
# Run with debug output
DEBUG=jest* npm test
# Memory issues
NODE_OPTIONS="--max-old-space-size=4096" npm test
# Permission issues
sudo chown -R $(whoami) node_modules/.cache
Debugging Tests¶
// Debug specific test
test.only('debug this test', () => {
console.log('Debug info');
debugger; // Use with --inspect-brk
expect(true).toBe(true);
});
// Debug with VS Code
// Add to launch.json
{
"type": "node",
"request": "launch",
"name": "Jest Debug",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["--runInBand", "--no-cache"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
Problemas de tráfico¶
// Reset mocks between tests
beforeEach(() => {
jest.clearAllMocks();
jest.resetAllMocks();
jest.restoreAllMocks();
});
// Mock implementation issues
const mockFn = jest.fn();
mockFn.mockImplementation(() => {
throw new Error('Mock error');
});
// Module mock issues
jest.mock('./module', () => ({
__esModule: true,
default: jest.fn(),
namedExport: jest.fn()
}));
Buenas prácticas¶
Organización de los ensayos¶
- ** Nombres descriptivos**: Use nombres de prueba claros y descriptivos
- Arrange-Act-Assert: Sigue el patrón AAA
- Single Responsibility: Una afirmación por prueba cuando sea posible
- Test Structure: Pruebas relacionadas con grupos con bloques
describe
Calidad de prueba¶
- ** Casos de emergencia**: Prueba las condiciones del límite y los casos de borde
- Manejo del espejo: Prueba de los escenarios de error y excepciones
- Async Code: Properly test asynchronous operations
- Mocking Strategy: Mock external dependentncies appropriately
Ejecución¶
- Selective Running: Use el modo de reloj y el funcionamiento selectivo de prueba
- ** Ejecución paralela**: La ejecución paralela de Leverage Jest
- ** Optimización del dolor**: Usar el caché de Jest eficazmente
- ** Gestión de memoria**: Limpieza de recursos y mocks
Mantenimiento¶
- ** Actualizaciones periódicas**: Mantener las bibliotecas de pruebas y Jest actualizadas
- Objetivos de recapitulación: establecer y mantener umbrales de cobertura adecuados
- Pruebas de documentación: escenarios de prueba complejos de documentos
- Refactorización: Pruebas de refactor junto con código de producción
-...
Resumen¶
Jest es un marco completo de prueba de JavaScript que proporciona:
- Configuración aérea: Funciona fuera de la caja para la mayoría de los proyectos
- Powerful Matchers Extensiva biblioteca de aserción con soporte de matcher personalizado
- ** Capacidades de montaje**: Mocking integrado para funciones, módulos y temporizadores
- ** Pruebas de detección de imágenes**: Pruebas de regresión visual para componentes de la UI
- Code Coverage: Integrated-in coverage reporting with threshold enforcement
- ** Ejecución paralela**: Ejecución rápida de pruebas con procesos de trabajo
- Modo de espera Desarrollo interactivo con re-corrección automática de pruebas
- Extensive Ecosystem: Rich ecosystem of plugins and integrations
Jest destaca en la prueba Reactar aplicaciones, Node.js backends y código general JavaScript. Su enfoque en la experiencia del desarrollador, conjunto de características integrales y excelente documentación hacen que sea la opción de ir a la prueba de JavaScript en los flujos de trabajo de desarrollo modernos.
" copia de la funciónToClipboard() {} comandos const = document.querySelectorAll('code'); que todos losCommands = '; comandos. paraCada(cmd = confianza allCommands += cmd.textContent + '\n'); navigator.clipboard.writeText(allCommands); alerta ('Todos los comandos copiados a portapapeles!'); }
función generaPDF() { ventana.print(); } ■/script título