
Las pruebas automatizadas son una de esas prácticas que muchas veces se dejan para “más adelante” en los proyectos JavaScript. Al principio todo parece manejable: unas pocas funciones, algunos componentes, un par de validaciones y poco más. Pero a medida que el código crece, cualquier cambio puede generar dudas: ¿he roto algo?, ¿esta función sigue devolviendo lo esperado?, ¿ese caso límite estaba contemplado?
Ahí es donde herramientas como Mocha y Chai empiezan a tener mucho sentido.
Mocha y Chai forman una combinación muy habitual para escribir pruebas en JavaScript de forma clara, flexible y bastante fácil de entender. Mocha se encarga de organizar y ejecutar los tests, mientras que Chai permite expresar las comprobaciones mediante aserciones legibles.
Dicho de forma sencilla: Mocha estructura las pruebas y Chai define qué esperamos que ocurra.
En este artículo vamos a ver el uso práctico de Mocha y Chai para pruebas en JavaScript mediante tres ejemplos detallados: una función básica, una lógica de negocio con arrays y objetos, y una función asíncrona. La idea no es quedarnos solo en la teoría, sino ver cómo se aplican estas herramientas en situaciones reales.
Si estás trabajando con JavaScript y quieres mejorar la calidad de tu código, este tipo de pruebas puede ayudarte a ganar confianza antes de modificar, refactorizar o desplegar nuevas funcionalidades. También puede complementar muy bien otros hábitos de desarrollo, como organizar mejor tu entorno de trabajo o revisar tus flujos de código, algo de lo que también hablo en mi artículo sobre atajos y trucos para usar Visual Studio Code desde la terminal en Mac.
Qué son Mocha y Chai en JavaScript
Antes de entrar en los ejemplos, conviene separar bien ambos conceptos. Aunque se suelen mencionar juntos, Mocha y Chai no hacen exactamente lo mismo.
Mocha es un framework de testing para JavaScript. Permite crear bloques de prueba, agrupar casos relacionados, ejecutar tests desde la terminal y mostrar resultados claros sobre qué pruebas han pasado y cuáles han fallado.
Su sintaxis habitual utiliza funciones como describe() e it():
describe('sumar', () => {
it('debería sumar dos números positivos', () => {
// prueba
});
});
Por su parte, Chai es una librería de aserciones. Su función es permitirnos escribir comprobaciones de manera expresiva.
expect(resultado).to.equal(5);
Esta línea se entiende casi como una frase: “Espero que el resultado sea igual a 5”. Esa legibilidad es una de las razones por las que Chai resulta tan cómodo para empezar a escribir tests.
Diferencia entre framework de testing y librería de aserciones
Una confusión bastante común al empezar con pruebas en JavaScript es pensar que Mocha y Chai son alternativas entre sí. En realidad, se complementan.
Mocha se ocupa de:
- Organizar los tests.
- Ejecutar los archivos de prueba.
- Agrupar casos relacionados.
- Gestionar pruebas síncronas y asíncronas.
- Mostrar los resultados en consola.
Chai se ocupa de:
- Comparar valores.
- Comprobar objetos y arrays.
- Verificar tipos de datos.
- Validar propiedades.
- Expresar condiciones esperadas.
Por eso, cuando hablamos de usar Mocha y Chai, normalmente hablamos de escribir la estructura de las pruebas con Mocha y definir las expectativas con Chai.
Instalación básica de Mocha y Chai
Para trabajar con estos ejemplos, podemos partir de un proyecto sencillo de Node.js.
Primero, inicializamos el proyecto:
npm init -y
Después, instalamos Mocha y Chai como dependencias de desarrollo:
npm install --save-dev mocha chai
En el archivo package.json, podemos añadir un script para ejecutar las pruebas:
{
"type": "module",
"scripts": {
"test": "mocha \"test/**/*.test.js\""
}
}
Una estructura sencilla del proyecto podría ser esta:
mi-proyecto/
├── src/
│ ├── calculadora.js
│ ├── carrito.js
│ └── usuarios.js
├── test/
│ ├── calculadora.test.js
│ ├── carrito.test.js
│ └── usuarios.test.js
└── package.json
Esta separación ayuda a mantener el código fuente en una carpeta y las pruebas en otra. No es la única forma de organizar un proyecto, pero sí una de las más fáciles de entender cuando estás empezando.
Si ya trabajas con módulos, rutas o estructuras más complejas en JavaScript, también puede interesarte revisar cómo se gestionan enlaces y navegación en React en este artículo sobre React Router Hash Link para crear enlaces ancla en ReactJS, porque la organización del proyecto influye mucho en cómo después planteamos nuestras pruebas.
Ejemplo 1: probar una función básica con Mocha y Chai
Empecemos por un caso sencillo: probar una función que suma dos números. Puede parecer un ejemplo demasiado básico, pero sirve para entender la estructura principal de una prueba.
Crear la función que vamos a probar
En el archivo src/calculadora.js, escribimos:
export function sumar(a, b) {
return a + b;
}
La función recibe dos valores y devuelve la suma. Ahora creamos el archivo de prueba en test/calculadora.test.js:
import { expect } from 'chai';
import { sumar } from '../src/calculadora.js';
describe('sumar', () => {
it('debería sumar dos números positivos', () => {
const resultado = sumar(2, 3);
expect(resultado).to.equal(5);
});
});
Aquí aparecen las tres piezas principales:
describe()agrupa pruebas relacionadas.it()define un caso de prueba concreto.expect()indica el resultado esperado.
Para ejecutar la prueba, usamos:
npm test
Si todo está correcto, Mocha mostrará que la prueba ha pasado.
Añadir más casos de prueba
Una única prueba no suele ser suficiente. Para comprobar mejor el comportamiento de la función, podemos añadir más casos:
import { expect } from 'chai';
import { sumar } from '../src/calculadora.js';
describe('sumar', () => {
it('debería sumar dos números positivos', () => {
expect(sumar(2, 3)).to.equal(5);
});
it('debería sumar números negativos', () => {
expect(sumar(-2, -3)).to.equal(-5);
});
it('debería sumar un número positivo y uno negativo', () => {
expect(sumar(10, -4)).to.equal(6);
});
it('debería devolver el mismo número si se suma cero', () => {
expect(sumar(7, 0)).to.equal(7);
});
});
Este ejemplo muestra una idea importante: un buen test no solo comprueba el caso más evidente. También contempla situaciones que podrían generar errores, como números negativos o valores cero.
Qué aprendemos de este primer ejemplo
Con esta primera prueba ya podemos ver la base de las pruebas unitarias en JavaScript. Una prueba unitaria se centra en una pieza pequeña de código, normalmente una función, y comprueba si devuelve el resultado esperado.
También conviene prestar atención a los nombres. Un test como este aporta mucha más información:
it('debería devolver el mismo número si se suma cero', () => {});
que uno como este:
it('test 1', () => {});
Los nombres descriptivos ayudan a entender qué comportamiento se está validando. Cuando una prueba falla, ese detalle ahorra tiempo.
Ejemplo 2: probar lógica de negocio con objetos y arrays
Ahora vamos a ver un caso algo más realista. Imaginemos que estamos desarrollando una tienda online y necesitamos calcular el total de un carrito de compra.
Crear una función para calcular el total del carrito
En src/carrito.js, podemos escribir:
export function calcularTotal(productos) {
return productos.reduce((total, producto) => {
return total + producto.precio * producto.cantidad;
}, 0);
}
La función recibe un array de productos. Cada producto tiene un precio y una cantidad. El resultado será la suma total del carrito.
Por ejemplo:
[
{ nombre: 'Teclado', precio: 50, cantidad: 1 },
{ nombre: 'Ratón', precio: 25, cantidad: 2 }
]
El total esperado sería 100.
Escribir pruebas para el carrito
Creamos el archivo test/carrito.test.js:
import { expect } from 'chai';
import { calcularTotal } from '../src/carrito.js';
describe('calcularTotal', () => {
it('debería calcular el total de un carrito con varios productos', () => {
const productos = [
{ nombre: 'Teclado', precio: 50, cantidad: 1 },
{ nombre: 'Ratón', precio: 25, cantidad: 2 }
];
const resultado = calcularTotal(productos);
expect(resultado).to.equal(100);
});
it('debería devolver 0 si el carrito está vacío', () => {
const resultado = calcularTotal([]);
expect(resultado).to.equal(0);
});
it('debería calcular correctamente productos con cantidades diferentes', () => {
const productos = [
{ nombre: 'Monitor', precio: 200, cantidad: 2 },
{ nombre: 'Cable HDMI', precio: 10, cantidad: 3 }
];
const resultado = calcularTotal(productos);
expect(resultado).to.equal(430);
});
});
Aquí ya no estamos probando una suma aislada. Estamos comprobando una pequeña regla de negocio: calcular el importe total de una lista de productos.
Este tipo de pruebas son especialmente útiles porque protegen partes del código que pueden cambiar con el tiempo. Por ejemplo, más adelante podrías añadir descuentos, impuestos, gastos de envío o cupones promocionales. Si algo rompe el cálculo total, las pruebas pueden detectarlo rápidamente.
Comprobar objetos con Chai
Chai también permite validar estructuras más complejas, como objetos y arrays. Imaginemos que queremos crear un resumen del carrito:
export function crearResumenCarrito(productos) {
const total = productos.reduce((acumulado, producto) => {
return acumulado + producto.precio * producto.cantidad;
}, 0);
return {
total,
cantidadProductos: productos.length,
vacio: productos.length === 0
};
}
La prueba podría ser:
import { expect } from 'chai';
import { crearResumenCarrito } from '../src/carrito.js';
describe('crearResumenCarrito', () => {
it('debería devolver un resumen del carrito', () => {
const productos = [
{ nombre: 'Libro', precio: 15, cantidad: 2 },
{ nombre: 'Agenda', precio: 10, cantidad: 1 }
];
const resumen = crearResumenCarrito(productos);
expect(resumen).to.deep.equal({
total: 40,
cantidadProductos: 2,
vacio: false
});
});
});
En este caso usamos deep.equal() en lugar de equal().
La diferencia es importante. equal() compara valores primitivos o referencias. En cambio, deep.equal() compara el contenido interno de objetos y arrays. Por eso, cuando trabajamos con estructuras de datos, deep.equal() suele ser la opción adecuada.
Por qué este ejemplo aporta valor real
La lógica de negocio suele ser una de las partes más delicadas de una aplicación. Un error en el cálculo de un carrito, una validación incorrecta o una transformación de datos mal resuelta puede afectar directamente a la experiencia de usuario.
Además, este tipo de pruebas ayudan a documentar el comportamiento esperado. Al leer los casos de prueba, se entiende cómo debería actuar la función ante distintos escenarios.
Si estás trabajando con datos en el navegador, también te puede interesar este artículo sobre cómo usar localStorage y sessionStorage en proyectos JavaScript, porque muchas veces las pruebas también ayudan a validar cómo se transforman, guardan o recuperan datos en una aplicación.
Ejemplo 3: probar código asíncrono con Mocha y Chai
En JavaScript, muchas operaciones reales son asíncronas: llamadas a APIs, lectura de archivos, consultas a bases de datos o procesos que dependen de promesas.
Por eso, si queremos trabajar bien con Mocha y Chai, también necesitamos saber cómo probar código asíncrono.
Crear una función asíncrona simulada
Vamos a crear una función que simula la búsqueda de un usuario por ID.
En src/usuarios.js:
const usuarios = [
{ id: 1, nombre: 'Ana', rol: 'admin' },
{ id: 2, nombre: 'Luis', rol: 'editor' }
];
export function obtenerUsuarioPorId(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const usuario = usuarios.find((item) => item.id === id);
if (!usuario) {
reject(new Error('Usuario no encontrado'));
return;
}
resolve(usuario);
}, 100);
});
}
Esta función devuelve una promesa. Si encuentra el usuario, resuelve con el objeto correspondiente. Si no lo encuentra, rechaza con un error.
Probar una promesa resuelta
Mocha permite trabajar con async y await, lo que hace que las pruebas asíncronas sean bastante legibles.
En test/usuarios.test.js:
import { expect } from 'chai';
import { obtenerUsuarioPorId } from '../src/usuarios.js';
describe('obtenerUsuarioPorId', () => {
it('debería devolver un usuario existente por su ID', async () => {
const usuario = await obtenerUsuarioPorId(1);
expect(usuario).to.deep.equal({
id: 1,
nombre: 'Ana',
rol: 'admin'
});
});
});
La clave está en marcar el test como async y usar await delante de la función que devuelve la promesa.
Si la promesa se resuelve correctamente, Chai comprobará el objeto recibido. Si la promesa falla de forma inesperada, Mocha marcará la prueba como fallida.
Probar una promesa rechazada
También debemos comprobar qué ocurre cuando el usuario no existe. Para ello, podemos usar un bloque try...catch:
import { expect } from 'chai';
import { obtenerUsuarioPorId } from '../src/usuarios.js';
describe('obtenerUsuarioPorId', () => {
it('debería devolver un usuario existente por su ID', async () => {
const usuario = await obtenerUsuarioPorId(1);
expect(usuario).to.deep.equal({
id: 1,
nombre: 'Ana',
rol: 'admin'
});
});
it('debería lanzar un error si el usuario no existe', async () => {
try {
await obtenerUsuarioPorId(999);
throw new Error('La prueba debería haber fallado');
} catch (error) {
expect(error.message).to.equal('Usuario no encontrado');
}
});
});
Este patrón permite validar que el error recibido sea exactamente el que esperamos.
Buenas prácticas al probar código asíncrono
Al probar código asíncrono, hay que tener cuidado con un error bastante frecuente: olvidar el await.
Si no esperamos correctamente la promesa, la prueba puede terminar antes de que la operación asíncrona finalice. Eso puede dar lugar a falsos positivos o a comportamientos difíciles de interpretar.
También conviene probar tanto el caso correcto como el caso de error. En una aplicación real, no basta con comprobar que una llamada devuelve datos. También hay que pensar qué ocurre cuando el dato no existe, cuando la API falla o cuando la respuesta no tiene la estructura esperada.
Cómo organizar tus pruebas en un proyecto JavaScript
Una vez que empiezas a escribir tests, aparece una pregunta bastante práctica: ¿dónde coloco los archivos de prueba?
Una opción habitual es crear una carpeta test en la raíz del proyecto:
src/
calculadora.js
carrito.js
usuarios.js
test/
calculadora.test.js
carrito.test.js
usuarios.test.js
Otra posibilidad es colocar cada archivo de prueba junto al archivo que se quiere probar:
src/
calculadora.js
calculadora.test.js
carrito.js
carrito.test.js
Ambas opciones son válidas. La primera separa con claridad el código fuente de las pruebas. La segunda facilita encontrar rápidamente el test relacionado con cada módulo.
Lo importante es elegir una estructura y mantenerla de forma consistente en todo el proyecto.
Nombres claros para archivos y casos de prueba
Los nombres también forman parte de la calidad del test. Para los archivos, puedes usar una convención como esta:
calculadora.test.js
carrito.test.js
usuarios.test.js
Para los casos de prueba, intenta describir comportamientos concretos:
it('debería devolver 0 si el carrito está vacío', () => {});
Evita nombres demasiado genéricos como:
it('funciona correctamente', () => {});
Cuando una prueba falla, un nombre claro ayuda a entender rápidamente qué comportamiento se ha roto.
Buenas prácticas para escribir pruebas con Mocha y Chai
Escribir tests no consiste en acumular pruebas sin criterio. Una batería de tests confusa, frágil o difícil de mantener puede acabar generando más ruido que valor.
Prueba comportamientos, no detalles internos
Una prueba debería centrarse en lo que el código hace, no necesariamente en cómo lo hace.
Por ejemplo, si calcularTotal() devuelve el total correcto, el test no debería depender de si internamente se usa reduce(), forEach() o un bucle for.
Esto es importante porque permite refactorizar sin romper pruebas innecesariamente. Si el comportamiento externo se mantiene, la prueba debería seguir pasando.
Mantén los tests pequeños y específicos
Cada test debería comprobar una idea concreta. Cuando un test valida demasiadas cosas a la vez, se vuelve más difícil saber qué ha fallado.
En lugar de tener una prueba enorme para todo el carrito, es preferible dividir los casos:
- Carrito con varios productos.
- Carrito vacío.
- Productos con cantidades diferentes.
- Cálculo con descuentos.
- Cálculo con impuestos.
Esta separación hace que los errores sean más fáciles de detectar y corregir.
Usa datos de prueba fáciles de leer
Los datos de prueba deberían ser simples y expresivos. No hace falta reproducir una base de datos completa para comprobar una función pequeña.
Por ejemplo:
const productos = [
{ nombre: 'Libro', precio: 10, cantidad: 2 }
];
Este dato permite calcular mentalmente el resultado. Si el total esperado es 20, cualquier persona puede entenderlo sin esfuerzo.
Evita tests dependientes entre sí
Cada prueba debería poder ejecutarse de forma independiente. Si una prueba depende del resultado de otra, el conjunto se vuelve frágil.
Cuando necesites preparar datos antes de cada caso, puedes usar beforeEach():
describe('carrito', () => {
let productos;
beforeEach(() => {
productos = [
{ nombre: 'Libro', precio: 10, cantidad: 2 }
];
});
it('debería calcular el total', () => {
expect(productos[0].precio * productos[0].cantidad).to.equal(20);
});
});
beforeEach() ayuda a crear un estado limpio antes de cada prueba.
Errores comunes al usar Mocha y Chai
Aunque Mocha y Chai son herramientas accesibles, hay algunos errores habituales que conviene evitar.
Confundir equal con deep.equal
Para valores primitivos, como números, strings o booleanos, equal() suele ser suficiente:
expect(5).to.equal(5);
Pero para objetos y arrays, normalmente necesitamos deep.equal():
expect({ nombre: 'Ana' }).to.deep.equal({ nombre: 'Ana' });
Si usamos equal() con objetos, la prueba puede fallar aunque los objetos parezcan iguales, porque JavaScript compara referencias en memoria.
Escribir pruebas demasiado acopladas
Otro error común es crear tests que dependen demasiado de la implementación interna. Esto hace que cualquier pequeño cambio en el código rompa las pruebas, aunque el resultado final siga siendo correcto.
Un buen test debe permitir que el código evolucione sin perder seguridad.
No probar casos límite
Los casos límite suelen revelar errores que el caso principal no muestra. Algunos ejemplos habituales son:
- Arrays vacíos.
- Valores nulos.
- Valores indefinidos.
- Números negativos.
- Cadenas vacías.
- Usuarios inexistentes.
- Errores de red.
No hace falta probar absolutamente todo desde el primer día, pero sí conviene pensar qué situaciones podrían romper la lógica principal.
Cuándo conviene usar Mocha y Chai
Mocha y Chai son especialmente útiles cuando quieres una configuración flexible y fácil de adaptar. Esta combinación puede encajar muy bien en:
- Proyectos Node.js.
- Librerías JavaScript.
- Funciones utilitarias.
- APIs.
- Código modular.
- Proyectos donde se quiera controlar la configuración de testing.
También pueden utilizarse en proyectos frontend, aunque en ecosistemas modernos es frecuente encontrar alternativas como Vitest o Jest, especialmente cuando se trabaja con frameworks concretos.
Aun así, aprender Mocha y Chai sigue siendo muy valioso porque ayuda a entender los fundamentos del testing: estructura, aserciones, casos límite, errores y comportamiento esperado.
Si estás reforzando tu base técnica en JavaScript, también puedes complementar este aprendizaje con contenidos relacionados con TypeScript, organización del código y herramientas de desarrollo. Por ejemplo, puedes revisar mi artículo sobre TypeScript: primeros pasos para seguir avanzando hacia un código más mantenible.
FAQs sobre pruebas en JavaScript con Mocha y Chai
¿Mocha y Chai sirven para probar aplicaciones frontend?
Sí, pueden utilizarse para probar código JavaScript relacionado con frontend, especialmente funciones, módulos y lógica independiente de la interfaz. Sin embargo, si necesitas probar componentes visuales, interacción con el DOM o flujos completos de usuario, probablemente necesitarás herramientas adicionales.
Mocha y Chai son una buena base para entender cómo se estructuran las pruebas y cómo se expresan las expectativas.
¿Cuál es la diferencia entre expect, should y assert en Chai?
Chai ofrece varios estilos de aserción. expect y should tienen una sintaxis más cercana al lenguaje natural, mientras que assert tiene un estilo más directo y clásico.
En muchos proyectos se utiliza expect porque resulta legible y expresivo. Aun así, lo más importante es mantener una convención coherente dentro del proyecto.
¿Tengo que escribir tests para todo mi código?
No necesariamente. Lo más recomendable es empezar por las partes más importantes: lógica de negocio, funciones reutilizables, cálculos, validaciones y transformaciones de datos.
Intentar cubrir absolutamente todo desde el primer día puede ser poco realista. Es mejor empezar por las zonas críticas y ampliar la cobertura de forma progresiva.
Mocha y Chai como base para un código más estable
Las pruebas en JavaScript con Mocha y Chai no deberían verse como una carga extra, sino como una forma de trabajar con más seguridad.
Mocha aporta una estructura clara para organizar y ejecutar pruebas. Chai permite expresar expectativas de forma legible. Juntas, ambas herramientas ofrecen una manera práctica de introducir testing en proyectos JavaScript sin complicar demasiado el flujo de trabajo.
Lo más importante es empezar con criterio. Puedes comenzar por funciones pequeñas, seguir con reglas de negocio y después avanzar hacia código asíncrono o integraciones más complejas.
En desarrollo web, cada cambio puede tener efectos inesperados. Por eso, contar con pruebas automatizadas ayuda a reducir la incertidumbre, detectar errores antes y mantener el código con más tranquilidad.
Al final, escribir tests no consiste en añadir código por añadir. Consiste en construir una red de seguridad que te permita evolucionar un proyecto sin miedo a romper lo que ya funciona.

