TL;DR

  • Jest es un framework de testing sin configuración que incluye assertions, mocking y cobertura
  • Matchers como toBe, toEqual, toContain hacen assertions legibles
  • Mockea funciones con jest.fn(), módulos con jest.mock(), timers con jest.useFakeTimers()
  • Testing async: usa async/await, resolves/rejects, o callback done
  • Snapshot testing captura UI — útil para componentes React

Ideal para: Desarrolladores JavaScript/TypeScript, proyectos React/Vue/Node.js, equipos queriendo todo-en-uno Omite si: Necesitas testing basado en navegador (usa Playwright/Cypress en su lugar)

Tu suite de tests tarda 10 minutos. La mitad de los tests son flaky. Nadie confía en los resultados.

Jest cambia esto. Es rápido, confiable y funciona de caja. Sin infierno de configuración. Sin armar cinco librerías diferentes.

Este tutorial enseña Jest desde cero — matchers, mocking, testing async y los patrones que hacen tests mantenibles.

¿Qué es Jest?

Jest es un framework de testing JavaScript creado por Facebook. Ejecuta tus tests, provee assertions, mockea dependencias y genera reportes de cobertura — todo en un paquete.

Qué incluye Jest:

  • Test runner — encuentra y ejecuta archivos de test
  • Librería de assertionsexpect() con matchers incluidos
  • Mocking — mock de funciones, módulos, timers
  • Cobertura — reportes de cobertura de código incluidos
  • Snapshot testing — captura y compara output
  • Watch mode — re-ejecuta tests al cambiar archivos

Instalación y Setup

Proyecto Nuevo

# Inicializar proyecto
npm init -y

# Instalar Jest
npm install --save-dev jest

# Agregar script de test a package.json
npm pkg set scripts.test="jest"

Proyecto TypeScript

npm install --save-dev jest typescript ts-jest @types/jest

# Inicializar config de ts-jest
npx ts-jest config:init

Create React App

Jest ya está incluido. Solo ejecuta:

npm test

Estructura del Proyecto

my-project/
├── src/
│   ├── calculator.js
│   └── utils/
│       └── formatters.js
├── __tests__/
│   ├── calculator.test.js
│   └── utils/
│       └── formatters.test.js
├── jest.config.js
└── package.json

Jest encuentra tests en:

  • Archivos terminando en .test.js o .spec.js
  • Archivos en carpetas __tests__

Escribiendo Tu Primer Test

// src/calculator.js
function add(a, b) {
  return a + b;
}

function divide(a, b) {
  if (b === 0) {
    throw new Error('Cannot divide by zero');
  }
  return a / b;
}

module.exports = { add, divide };
// __tests__/calculator.test.js
const { add, divide } = require('../src/calculator');

describe('Calculator', () => {
  describe('add', () => {
    test('suma dos números positivos', () => {
      expect(add(2, 3)).toBe(5);
    });

    test('suma números negativos', () => {
      expect(add(-1, -1)).toBe(-2);
    });

    test('suma con cero', () => {
      expect(add(5, 0)).toBe(5);
    });
  });

  describe('divide', () => {
    test('divide dos números', () => {
      expect(divide(10, 2)).toBe(5);
    });

    test('lanza error al dividir por cero', () => {
      expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
    });
  });
});

Ejecutando Tests

# Ejecutar todos los tests
npm test

# Ejecutar archivo específico
npm test -- calculator.test.js

# Ejecutar tests que coincidan con patrón
npm test -- --testNamePattern="suma"

# Watch mode
npm test -- --watch

# Con cobertura
npm test -- --coverage

Matchers

Los matchers son métodos para verificar valores. Jest tiene 50+ matchers incluidos.

Matchers Comunes

// Igualdad
expect(2 + 2).toBe(4);                    // Igualdad estricta (===)
expect({ a: 1 }).toEqual({ a: 1 });       // Igualdad profunda

// Truthiness
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('value').toBeDefined();

// Números
expect(10).toBeGreaterThan(5);
expect(10).toBeGreaterThanOrEqual(10);
expect(5).toBeLessThan(10);
expect(0.1 + 0.2).toBeCloseTo(0.3);       // Punto flotante

// Strings
expect('Hello World').toMatch(/World/);
expect('Hello World').toContain('World');

// Arrays
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toHaveLength(3);
expect(['a', 'b']).toEqual(expect.arrayContaining(['a']));

// Objetos
expect({ a: 1, b: 2 }).toHaveProperty('a');
expect({ a: 1, b: 2 }).toHaveProperty('a', 1);
expect({ a: 1 }).toMatchObject({ a: 1 });

// Excepciones
expect(() => { throw new Error('fail'); }).toThrow();
expect(() => { throw new Error('fail'); }).toThrow('fail');
expect(() => { throw new Error('fail'); }).toThrow(Error);

Negando Matchers

Agrega .not antes de cualquier matcher:

expect(5).not.toBe(3);
expect([1, 2]).not.toContain(3);
expect({ a: 1 }).not.toHaveProperty('b');

Testing de Código Async

Async/Await

// src/api.js
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error('User not found');
  }
  return response.json();
}

module.exports = { fetchUser };
// __tests__/api.test.js
test('obtiene usuario exitosamente', async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe('John');
});

test('lanza error para usuario inválido', async () => {
  await expect(fetchUser(999)).rejects.toThrow('User not found');
});

Promesas con resolves/rejects

test('resuelve a datos de usuario', () => {
  return expect(fetchUser(1)).resolves.toMatchObject({ name: 'John' });
});

test('rechaza para usuario faltante', () => {
  return expect(fetchUser(999)).rejects.toThrow();
});

Estilo Callback (done)

function fetchDataWithCallback(callback) {
  setTimeout(() => {
    callback({ data: 'result' });
  }, 100);
}

test('llama callback con datos', (done) => {
  function callback(result) {
    expect(result.data).toBe('result');
    done();  // Test espera hasta que se llame done()
  }
  fetchDataWithCallback(callback);
});

Mocking

Mocking reemplaza implementaciones reales con sustitutos controlados.

Mock Functions (jest.fn)

test('mock function trackea llamadas', () => {
  const mockCallback = jest.fn(x => x + 1);

  [1, 2, 3].forEach(mockCallback);

  // Verificar cantidad de llamadas
  expect(mockCallback).toHaveBeenCalledTimes(3);

  // Verificar llamadas específicas
  expect(mockCallback).toHaveBeenCalledWith(1);
  expect(mockCallback).toHaveBeenLastCalledWith(3);

  // Verificar valores de retorno
  expect(mockCallback.mock.results[0].value).toBe(2);
});

Mock Valores de Retorno

const mock = jest.fn();

// Retornar diferentes valores en llamadas sucesivas
mock
  .mockReturnValueOnce(10)
  .mockReturnValueOnce(20)
  .mockReturnValue(30);

console.log(mock()); // 10
console.log(mock()); // 20
console.log(mock()); // 30
console.log(mock()); // 30

// Mock promesas resolved/rejected
const asyncMock = jest.fn()
  .mockResolvedValueOnce({ success: true })
  .mockRejectedValueOnce(new Error('Failed'));

Mocking Módulos

// Mock módulo completo
jest.mock('./api');

const { fetchUser } = require('./api');

// Configurar implementación mock
fetchUser.mockResolvedValue({ id: 1, name: 'Mock User' });

test('usa API mockeado', async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe('Mock User');
});

Mock con Factory

jest.mock('./database', () => ({
  connect: jest.fn().mockResolvedValue(true),
  query: jest.fn().mockResolvedValue([{ id: 1 }]),
  close: jest.fn()
}));

Espiando Métodos

const video = {
  play() {
    return true;
  }
};

test('espía método', () => {
  const spy = jest.spyOn(video, 'play');

  video.play();

  expect(spy).toHaveBeenCalled();
  expect(spy).toHaveReturnedWith(true);

  spy.mockRestore();  // Restaurar implementación original
});

Mocking Timers

jest.useFakeTimers();

function delayedGreeting(callback) {
  setTimeout(() => callback('Hello'), 1000);
}

test('llama callback después de delay', () => {
  const callback = jest.fn();

  delayedGreeting(callback);

  expect(callback).not.toHaveBeenCalled();

  jest.advanceTimersByTime(1000);  // Avanzar tiempo

  expect(callback).toHaveBeenCalledWith('Hello');
});

// O ejecutar todos los timers
test('con runAllTimers', () => {
  const callback = jest.fn();

  delayedGreeting(callback);
  jest.runAllTimers();

  expect(callback).toHaveBeenCalled();
});

Setup y Teardown

describe('Database tests', () => {
  let db;

  // Ejecuta una vez antes de todos los tests en este describe
  beforeAll(async () => {
    db = await connectToDatabase();
  });

  // Ejecuta una vez después de todos los tests
  afterAll(async () => {
    await db.close();
  });

  // Ejecuta antes de cada test
  beforeEach(async () => {
    await db.clear();
  });

  // Ejecuta después de cada test
  afterEach(() => {
    jest.clearAllMocks();
  });

  test('inserta registro', async () => {
    await db.insert({ name: 'Test' });
    const records = await db.findAll();
    expect(records).toHaveLength(1);
  });
});

Snapshot Testing

Los snapshots capturan output y detectan cambios no intencionales.

// src/formatUser.js
function formatUser(user) {
  return {
    displayName: `${user.firstName} ${user.lastName}`,
    email: user.email.toLowerCase(),
    initials: `${user.firstName[0]}${user.lastName[0]}`
  };
}

module.exports = { formatUser };
// __tests__/formatUser.test.js
const { formatUser } = require('../src/formatUser');

test('formatea usuario correctamente', () => {
  const user = {
    firstName: 'John',
    lastName: 'Doe',
    email: 'John.Doe@Example.com'
  };

  expect(formatUser(user)).toMatchSnapshot();
});

Primera ejecución crea __snapshots__/formatUser.test.js.snap:

exports[`formatea usuario correctamente 1`] = `
{
  "displayName": "John Doe",
  "email": "john.doe@example.com",
  "initials": "JD"
}
`;

Si el output cambia, el test falla. Actualiza snapshots:

npm test -- --updateSnapshot
# o
npm test -- -u

Cobertura de Código

# Generar reporte de cobertura
npm test -- --coverage

# Con umbrales específicos
npm test -- --coverage --coverageThreshold='{"global":{"branches":80,"functions":80}}'

Configuración de Cobertura

// jest.config.js
module.exports = {
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.js'
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },
  coverageReporters: ['text', 'lcov', 'html']
};

Mejores Prácticas

1. Un Assertion Por Test (Usualmente)

// Malo: múltiples assertions no relacionados
test('validación de usuario', () => {
  expect(isValidEmail('test@example.com')).toBe(true);
  expect(isValidEmail('invalid')).toBe(false);
  expect(isValidName('John')).toBe(true);
});

// Bueno: tests separados
test('acepta email válido', () => {
  expect(isValidEmail('test@example.com')).toBe(true);
});

test('rechaza email inválido', () => {
  expect(isValidEmail('invalid')).toBe(false);
});

2. Nombres de Tests Descriptivos

// Malo
test('test1', () => { ... });

// Bueno
test('retorna null cuando user ID no se encuentra', () => { ... });

3. Patrón Arrange-Act-Assert

test('calcula total con descuento', () => {
  // Arrange
  const cart = { items: [{ price: 100 }, { price: 50 }] };
  const discount = 0.1;

  // Act
  const total = calculateTotal(cart, discount);

  // Assert
  expect(total).toBe(135);
});

4. Evita Testear Detalles de Implementación

// Malo: testea estado interno
test('establece flag interno', () => {
  const counter = new Counter();
  counter.increment();
  expect(counter._count).toBe(1);  // Testeando propiedad privada
});

// Bueno: testea comportamiento público
test('incrementa contador', () => {
  const counter = new Counter();
  counter.increment();
  expect(counter.getCount()).toBe(1);  // Testeando método público
});

IA en Jest Testing

Las herramientas de IA pueden acelerar la escritura de tests cuando se usan apropiadamente.

Lo que la IA hace bien:

  • Generar casos de test desde firmas de función
  • Crear datos mock con formas específicas
  • Escribir boilerplate para patrones comunes
  • Sugerir edge cases que podrías olvidar

Lo que aún necesita humanos:

  • Decidir qué vale la pena testear
  • Verificar que los tests realmente testean lo correcto
  • Escribir tests para lógica de negocio compleja
  • Debuggear tests fallidos

FAQ

¿Para qué se usa Jest?

Jest es un framework de testing JavaScript para unit tests, integration tests y snapshot testing. Provee test runner, librería de assertions, utilidades de mocking y cobertura de código — todo en un paquete. Jest funciona con React, Vue, Angular, Node.js y cualquier proyecto JavaScript o TypeScript.

¿Jest es solo para React?

No. Aunque Jest fue creado por Facebook y es popular en proyectos React, funciona con cualquier codebase JavaScript o TypeScript. Jest testea código backend Node.js, componentes Vue, aplicaciones Angular y JavaScript vanilla igualmente bien. React Testing Library es un paquete separado que complementa Jest para testing específico de React.

¿Cuál es la diferencia entre Jest y Mocha?

Jest es un framework todo-en-uno con assertions (expect), mocking (jest.fn) y cobertura incluidos. Mocha es un test runner que requiere librerías separadas: Chai para assertions, Sinon para mocking, nyc para cobertura. Jest es más fácil de configurar; Mocha ofrece más flexibilidad y personalización. Para proyectos nuevos, Jest suele ser la opción más simple.

¿Cómo mockear llamadas API en Jest?

Varios enfoques funcionan:

  1. jest.mock() — mockear todo el módulo fetch/axios
  2. jest.spyOn() — espiar y mockear métodos específicos
  3. Manual mocks — crear carpeta __mocks__ con implementaciones mock
  4. MSW (Mock Service Worker) — interceptar requests de red para mocking realista de API

¿Cómo configurar Jest con TypeScript?

Instala ts-jest y @types/jest, luego ejecuta npx ts-jest config:init para generar un archivo de configuración. Esto crea un jest.config.js con el preset de ts-jest. Para transformación más rápida en codebases grandes (500+ tests), usa @swc/jest en su lugar — es 20-70x más rápido que ts-jest porque omite la verificación de tipos durante la ejecución de tests.

¿Qué es snapshot testing en Jest?

Snapshot testing captura el output renderizado de un componente (o cualquier valor serializable) y lo guarda en un archivo .snap. En ejecuciones posteriores, Jest compara el output actual con el snapshot guardado y falla si difieren. Es útil para detectar cambios no intencionales en componentes React, respuestas de API u objetos de configuración. Actualiza snapshots con npm test -- -u cuando los cambios son intencionales.

Ver También