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) Tiempo de lectura: 15 minutos

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
});

Testing Jest con Asistencia de IA

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

Recursos Oficiales

Ver También