Lección 1 de 413 min
Pruebas unitarias: Vitest, Jest y Mocha
La pirámide de testing, la estructura de una prueba, dobles de prueba y los tres runners más usados.
Cambias la función que calcula los gastos de envío para añadir una tarifa nueva. Todo parece funcionar, pero sin darte cuenta has roto el envío gratis a partir de 50 €, y lo descubre un cliente tres días después. Una prueba automática de cinco líneas te lo habría dicho en un segundo, nada más guardar el archivo.
Las pruebas unitarias comprueban piezas pequeñas de código, como una función o una clase, de forma aislada y muy rápida. En esta lección verás cuántas pruebas de cada tipo conviene tener, cómo se escribe una buena prueba, qué son los dobles de prueba y en qué se diferencian Vitest, Jest y Mocha.
Qué probar y cómo
La pirámide de testing
Muchas pruebas pequeñas y rápidas en la base, y pocas lentas y caras en la cima.
- Popularizada por:
- Mike Cohn
- Variante:
- trofeo de testing
La pirámide, popularizada por Mike Cohn, reparte las pruebas en tres niveles. En la base, muchas pruebas unitarias: rápidas, baratas y precisas al señalar el error. En medio, menos pruebas de integración, que comprueban que varias piezas funcionan juntas, como tu código con la base de datos o un componente con sus hijos. En la cima, unas pocas pruebas end-to-end, que recorren la aplicación entera en un navegador.
Cuanto más arriba, más confianza da cada prueba, pero también es más lenta, más cara de mantener y más difícil de diagnosticar. Por eso importa la forma: una pirámide invertida, con casi todo en end-to-end, produce pipelines lentos y frágiles.
Kent C. Dodds propuso una variante, el trofeo de testing, que pone el análisis estático en la base y da más peso a las pruebas de integración, porque en las interfaces son las que mejor equilibran coste y confianza. Las dos ideas coinciden en lo esencial: elige el nivel más bajo que te dé una confianza real.
Úsalo cuando
Para decidir qué tipo de prueba escribir ante cada cambio y para detectar cuándo tu batería de pruebas se está desequilibrando.
Ten en cuenta
No es una regla con porcentajes exactos. Una función de cálculo pide pruebas unitarias; un flujo de pago, además, al menos una end-to-end. Lo que debes evitar son los extremos.
/\ End-to-end: pocas, lentas, la aplicación entera
/ \
/----\ Integración: varias piezas juntas
/ \
/--------\ Unitarias: muchas, rápidas, una pieza aisladaAnatomía de una prueba
Preparar, actuar y comprobar, dentro de bloques describe e it.
Casi todas las pruebas siguen el patrón Arrange-Act-Assert: preparas los datos (arrange), ejecutas lo que quieres probar (act) y compruebas el resultado (assert). Separar las tres fases hace que se entienda de un vistazo qué se está probando.
Los runners modernos comparten vocabulario: describe agrupa pruebas relacionadas, it (o su sinónimo test) define una prueba con una frase que describe el comportamiento esperado, y expect hace las comprobaciones con «matchers» como toBe (igualdad estricta) o toEqual (mismo contenido, para objetos y listas).
Cada prueba debe estar aislada: no puede depender del orden ni de lo que dejó otra. Si varias necesitan el mismo punto de partida, créalo en beforeEach, que se ejecuta antes de cada una.
Úsalo cuando
En cada prueba que escribas. Un buen nombre en el it funciona como documentación: «es gratis a partir de 50 €» explica la regla de negocio mejor que un comentario.
Ten en cuenta
Prueba el comportamiento, no los detalles internos: si cambias cómo se calcula algo sin cambiar el resultado, ninguna prueba debería romperse. Y desconfía de una prueba que nunca has visto fallar: quizá no esté comprobando nada.
import { describe, it, expect } from "vitest";
import { calcularEnvio } from "./envio";
describe("calcularEnvio", () => {
it("es gratis a partir de 50 €", () => {
// Arrange: prepara los datos
const carrito = { total: 60 };
// Act: ejecuta lo que pruebas
const envio = calcularEnvio(carrito);
// Assert: comprueba el resultado
expect(envio).toBe(0);
});
it("cuesta 4,95 € por debajo de 50 €", () => {
expect(calcularEnvio({ total: 20 })).toBe(4.95);
});
});Dobles de prueba: mocks, stubs y spies
Sustitutos de las dependencias reales para probar una pieza de forma aislada.
Si tu función cobra con una pasarela de pago o envía correos, no quieres que la prueba cobre de verdad. Un doble de prueba sustituye esa dependencia. Un stub devuelve respuestas preparadas («el pago siempre se acepta»). Un spy registra cómo se le llamó: cuántas veces y con qué argumentos. Un mock, en sentido estricto, es un doble con expectativas sobre esas llamadas.
En JavaScript, vi.fn() en Vitest y jest.fn() en Jest crean funciones falsas que hacen las tres cosas: puedes decirles qué devolver (mockReturnValue, mockResolvedValue) y después comprobar sus llamadas (toHaveBeenCalledWith). Con vi.spyOn vigilas un método real y con vi.mock sustituyes un módulo entero. Por eso, en el día a día, se llama «mock» a cualquier doble.
Úsalo cuando
Para lo que es lento, caro o impredecible: la red, la base de datos, los servicios externos, la hora actual o los números aleatorios.
Ten en cuenta
No abuses: si lo sustituyes todo, acabas probando tus mocks y no tu código, y las pruebas se rompen en cada refactor aunque el comportamiento siga igual. Sustituye los bordes del sistema y deja real la lógica.
import { it, expect, vi } from "vitest";
import { confirmarPedido } from "./pedidos";
it("cobra el total y envía un correo al confirmar", async () => {
// Stub: la pasarela de pago siempre acepta
const pasarela = { cobrar: vi.fn().mockResolvedValue({ ok: true }) };
// Spy: solo queremos saber si se llamó y con qué
const correo = { enviar: vi.fn() };
await confirmarPedido({ id: 7, total: 30 }, pasarela, correo);
expect(pasarela.cobrar).toHaveBeenCalledWith(30);
expect(correo.enviar).toHaveBeenCalledTimes(1);
});¿Sabías que…?
Escribir la prueba antes que el código se llama TDD (desarrollo guiado por pruebas): escribes una prueba que falla, el código mínimo para que pase y después mejoras el diseño. No es obligatorio, pero garantiza que cada prueba ha fallado al menos una vez y, por tanto, que comprueba algo.
Los runners: Vitest, Jest y Mocha
Un runner (o framework de pruebas) encuentra los archivos de prueba, los ejecuta y te informa del resultado. Los tres más usados en JavaScript comparten la sintaxis de describe e it, pero difieren en lo que incluyen y en cómo tratan TypeScript y los módulos modernos.
Vitest
El runner del ecosistema Vite: rápido, compatible con Jest y con TypeScript sin configurar.
- Creado por:
- el equipo de Vite
- API:
- compatible con Jest
Vitest nació en el equipo de Vite y reutiliza su configuración y su forma de transformar el código, así que TypeScript, JSX y los módulos ES (ESM) funcionan sin añadir nada. Su API es compatible con la de Jest: describe, it, expect y vi en lugar de jest, de modo que migrar suele ser sencillo.
En modo watch solo repite las pruebas afectadas por el archivo que guardas, lo que da una respuesta casi inmediata. Incluye mocks, temporizadores falsos, snapshots, cobertura y, en las versiones recientes, un modo que ejecuta las pruebas en un navegador real. Angular, en sus versiones recientes, lo ha adoptado como runner por defecto para los proyectos nuevos, en lugar de Karma y Jasmine.
Úsalo cuando
En proyectos nuevos, sobre todo si ya usas Vite (Vue, Svelte, React con Vite) o una versión reciente de Angular.
Ten en cuenta
Que la API sea compatible no significa que todo sea idéntico: algunos detalles de los mocks de módulos y de la configuración difieren de Jest. Si migras un proyecto grande, sigue su guía de migración.
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "jsdom", // simula el DOM para probar componentes
coverage: { provider: "v8" }, // informe de cobertura con --coverage
},
});
// npx vitest → modo watch mientras desarrollas
// npx vitest run → una sola pasada, como en el pipelineJest
El runner todo en uno de Meta: aserciones, mocks, cobertura y snapshots incluidos.
- Creado por:
- Meta
- Mantiene:
- OpenJS Foundation
Jest, creado en Facebook (hoy Meta), popularizó la idea de un runner que lo trae todo: aserciones, mocks, cobertura, ejecución en paralelo y un entorno jsdom que simula el navegador dentro de Node para probar código que usa el DOM. Durante años fue la opción por defecto en React y sigue siéndolo en React Native. Hoy lo mantiene la OpenJS Foundation.
Su función más conocida son los snapshots: toMatchSnapshot() guarda el resultado la primera vez en un archivo y, en las ejecuciones siguientes, avisa si cambia. Es útil para detectar cambios inesperados en una estructura grande.
Úsalo cuando
En proyectos que ya lo usan, en React Native o cuando quieres una herramienta madura con una comunidad enorme.
Ten en cuenta
Para TypeScript y ESM necesita configuración extra (Babel o ts-jest), y su soporte de ESM nativo se considera experimental. Con los snapshots, cuidado con aceptar cambios a ciegas con jest -u: una snapshot que nadie lee no prueba nada.
const { formatearPrecio, resumirPedido } = require("./precio");
test("formatea el precio en euros", () => {
expect(formatearPrecio(30)).toBe("30,00 €");
});
test("el resumen del pedido no cambia sin querer", () => {
// La primera vez guarda el resultado; después lo compara
expect(resumirPedido({ productos: 3, total: 30 })).toMatchSnapshot();
});Mocha
Un runner veterano y flexible: tú eliges la librería de aserciones y la de mocks.
- Desde:
- 2011
- Se combina con:
- Chai y Sinon
Mocha, publicado en 2011, solo se encarga de encontrar y ejecutar las pruebas, con describe, it y los hooks before, after, beforeEach y afterEach. Lo demás lo eliges tú: lo habitual es combinarlo con Chai para las aserciones (expect(x).to.equal(3)) y con Sinon para spies, stubs y mocks.
Esa flexibilidad lo hizo muy popular en los backends con Node.js, y muchos proyectos veteranos lo siguen usando. Funciona tanto en Node como en el navegador y ofrece muchos formatos de informe (reporters).
Úsalo cuando
En proyectos que ya lo usan o cuando quieres elegir cada pieza de tu herramienta de pruebas.
Ten en cuenta
Más piezas significa más configuración y más dependencias que mantener. Para TypeScript necesitas un cargador como tsx, y sustituir módulos enteros es más complicado que con vi.mock.
import { expect } from "chai";
import sinon from "sinon";
import { registrarUsuario } from "../src/usuarios.js";
describe("registrarUsuario", () => {
afterEach(() => sinon.restore());
it("guarda el correo en minúsculas", async () => {
const db = { guardar: sinon.stub().resolves({ id: 1 }) };
await registrarUsuario(db, "Ana@Ejemplo.com");
expect(db.guardar.calledOnceWith({ email: "ana@ejemplo.com" })).to.be.true;
});
});Interactivo
Vitest, Jest y Mocha
Activa o desactiva opciones para compararlas cara a cara.
| Característica | Vitest | Jest | Mocha |
|---|---|---|---|
| Qué incluye | Runner, aserciones, mocks, snapshots y cobertura | Runner, aserciones, mocks, snapshots y cobertura | Solo el runner; añades Chai y Sinon |
| TypeScript y ESM | Funcionan sin configurar | Con Babel o ts-jest; ESM nativo experimental | ESM sí; TypeScript con un cargador |
| Mocks | vi.fn, vi.spyOn, vi.mock | jest.fn, jest.spyOn, jest.mock | Con Sinon |
| Modo watch | Repite solo lo afectado por cada cambio | Repite las pruebas relacionadas con los archivos cambiados | Básico, con --watch |
| Encaja con | Proyectos con Vite, Vue y Angular reciente | React Native y proyectos existentes | Backends de Node veteranos |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Empiezas un proyecto nuevo con Vite y TypeScript y quieres pruebas que arranquen al instante y se repitan al guardar, sin configurar Babel.
Para recordar
- La pirámide (o el trofeo) recuerda que las pruebas rápidas y baratas deben ser mayoría, y las end-to-end, pocas y para lo crítico.
- Una buena prueba sigue Arrange-Act-Assert, describe un comportamiento en su nombre y no depende de ninguna otra.
- Los dobles de prueba sustituyen lo lento o impredecible; si lo sustituyes todo, ya no pruebas nada real.
- Vitest es la opción moderna por defecto, Jest el todo en uno más extendido y Mocha el veterano que se combina con Chai y Sinon.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.¿Qué propone la pirámide de testing?
2.En una prueba, ¿a qué fase corresponde
const envio = calcularEnvio(carrito)?3.Creas
const avisar = vi.fn()y llamas a tu función pasándoleavisar. ¿Qué puedes comprobar después?4.¿Por qué Vitest funciona con TypeScript sin configuración extra?
5.Sustituyes con mocks todas las funciones internas de un módulo. ¿Cuál es el riesgo?