Saltar a contenido

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

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