Mocha y Chai siguen siendo una de las combinaciones de testing JavaScript más adoptadas, utilizadas por millones de desarrolladores para pruebas unitarias y de integración. Según la encuesta State of JS 2023, Mocha mantiene más del 50% de cuota de uso entre desarrolladores JavaScript y es la opción predeterminada en más del 60% de proyectos Node.js backend. Según estadísticas de descargas de NPM, mocha recibe más de 9 millones de descargas semanales. La combinación Mocha + Chai es particularmente poderosa por su diseño complementario: Mocha proporciona estructura flexible y ejecución de pruebas asíncronas, mientras Chai ofrece tres estilos de aserciones (assert, expect, should). Esta guía cubre setup, configuración avanzada y mejores prácticas.
TL;DR: Mocha + Chai es una combinación flexible para testing JavaScript: Mocha proporciona estructura de tests (describe/it/hooks before/after) y soporte async, mientras Chai ofrece aserciones legibles. Usa el estilo expect para mayor legibilidad, añade Sinon para mocking e Istanbul/nyc para cobertura de código.
Introducción
Mocha y Chai forman una de las combinaciones de testing más populares en el ecosistema JavaScript. Mocha proporciona un framework de testing flexible con excelente soporte asíncrono, mientras que Chai ofrece estilos de aserción expresivos que hacen que los tests sean legibles y mantenibles. Juntos, crean una solución de testing poderosa para aplicaciones Node.js y basadas en navegador.
Para equipos que buscan alternativas modernas, frameworks como Cypress y Playwright ofrecen experiencias de testing end-to-end completas con características adicionales. Si estás desarrollando una estrategia de automatización de pruebas, Mocha y Chai son excelentes para tests unitarios y de integración, mientras que testing continuo en DevOps muestra cómo integrar estas herramientas en pipelines CI/CD.
“Mocha’s flexibility is its biggest strength and its biggest trap. Without coding standards for your describe blocks, hooks, and assertion style, a large Mocha test suite becomes unmaintainable fast.” — Yuri Kan, Senior QA Lead
¿Por Qué Mocha y Chai?
Ventajas de Mocha
- Flexibilidad: Funciona con cualquier librería de aserciones (Chai, Should.js, Expect.js)
- Soporte asíncrono: Soporte de primera clase para Promesas, async/await y callbacks
- Reportes ricos: Múltiples reporters integrados y sistema de reporters extensible
- Soporte de navegador: Se ejecuta tanto en Node.js como en navegadores
- Ejecución serial: Los tests se ejecutan serialmente, permitiendo control preciso del flujo asíncrono
Ventajas de Chai
- Múltiples estilos de aserción: BDD (expect/should) y TDD (assert)
- Sintaxis legible: Aserciones en lenguaje natural
- Extensible: Rico ecosistema de plugins
- Mensajes de error: Mensajes de error claros y útiles
Instalación y Configuración
Instalación Básica
npm install --save-dev mocha chai
Estructura del Proyecto
project/
├── src/
│ └── calculator.js
├── test/
│ ├── unit/
│ │ └── calculator.test.js
│ └── integration/
│ └── api.test.js
├── package.json
└── .mocharc.json
Configuración (.mocharc.json)
{
"require": ["test/setup.js"],
"spec": ["test/**/*.test.js"],
"timeout": 5000,
"reporter": "spec",
"recursive": true,
"exit": true
}
Scripts de Package.json
{
"scripts": {
"test": "mocha",
"test:watch": "mocha --watch",
"test:coverage": "nyc mocha",
"test:unit": "mocha test/unit/**/*.test.js",
"test:integration": "mocha test/integration/**/*.test.js"
}
}
Estilos de Aserción de Chai
1. Expect (Estilo BDD)
const { expect } = require('chai');
describe('Estilo Expect', () => {
it('debería demostrar aserciones expect', () => {
const name = 'John';
const age = 30;
const hobbies = ['reading', 'coding'];
const user = { name: 'John', active: true };
// Igualdad
expect(name).to.equal('John');
expect(age).to.be.a('number');
// Igualdad profunda para objetos/arrays
expect(user).to.deep.equal({ name: 'John', active: true });
// Longitud e inclusión
expect(hobbies).to.have.lengthOf(2);
expect(hobbies).to.include('coding');
// Existencia de propiedad
expect(user).to.have.property('name');
expect(user).to.have.property('name', 'John');
// Verificaciones booleanas
expect(user.active).to.be.true;
expect(undefined).to.be.undefined;
expect(null).to.be.null;
// Negación
expect(name).to.not.equal('Jane');
});
});
2. Should (Estilo BDD)
const chai = require('chai');
chai.should();
describe('Estilo Should', () => {
it('debería demostrar aserciones should', () => {
const name = 'John';
const hobbies = ['reading', 'coding'];
name.should.equal('John');
name.should.be.a('string');
hobbies.should.have.lengthOf(2);
hobbies.should.include('coding');
});
});
3. Assert (Estilo TDD)
const { assert } = require('chai');
describe('Estilo Assert', () => {
it('debería demostrar aserciones assert', () => {
const name = 'John';
const user = { name: 'John', age: 30 };
assert.equal(name, 'John');
assert.typeOf(name, 'string');
assert.lengthOf(name, 4);
assert.property(user, 'name');
assert.propertyVal(user, 'name', 'John');
assert.deepEqual(user, { name: 'John', age: 30 });
});
});
Ejemplos de Testing del Mundo Real
Probando un Módulo Calculadora
src/calculator.js:
class Calculator {
add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Los argumentos deben ser números');
}
return a + b;
}
divide(a, b) {
if (b === 0) {
throw new Error('División por cero');
}
return a / b;
}
async asyncMultiply(a, b) {
return new Promise((resolve) => {
setTimeout(() => resolve(a * b), 100);
});
}
}
module.exports = Calculator;
test/unit/calculator.test.js:
const { expect } = require('chai');
const Calculator = require('../../src/calculator');
describe('Calculator', () => {
let calculator;
beforeEach(() => {
calculator = new Calculator();
});
describe('#add()', () => {
it('debería sumar dos números positivos', () => {
const result = calculator.add(5, 3);
expect(result).to.equal(8);
});
it('debería sumar números negativos', () => {
expect(calculator.add(-5, -3)).to.equal(-8);
});
it('debería lanzar TypeError para argumentos no numéricos', () => {
expect(() => calculator.add('5', 3)).to.throw(TypeError, 'Los argumentos deben ser números');
});
});
describe('#divide()', () => {
it('debería dividir dos números', () => {
expect(calculator.divide(10, 2)).to.equal(5);
});
it('debería lanzar error al dividir por cero', () => {
expect(() => calculator.divide(10, 0)).to.throw(Error, 'División por cero');
});
});
describe('#asyncMultiply()', () => {
it('debería multiplicar dos números asíncronamente', async () => {
const result = await calculator.asyncMultiply(4, 5);
expect(result).to.equal(20);
});
});
});
Hooks de Mocha
Tipos de Hooks
describe('Ejemplo de Hooks', () => {
before(() => {
// Se ejecuta una vez antes de todos los tests en este bloque
console.log('Configuración: antes de todos los tests');
});
after(() => {
// Se ejecuta una vez después de todos los tests en este bloque
console.log('Limpieza: después de todos los tests');
});
beforeEach(() => {
// Se ejecuta antes de cada test en este bloque
console.log('Configuración: antes de cada test');
});
afterEach(() => {
// Se ejecuta después de cada test en este bloque
console.log('Limpieza: después de cada test');
});
it('test 1', () => {
expect(true).to.be.true;
});
it('test 2', () => {
expect(false).to.be.false;
});
});
Hooks Asíncronos
describe('Hooks Asíncronos', () => {
before(async () => {
// Configurar conexión a base de datos
await database.connect();
});
after(async () => {
// Cerrar conexión a base de datos
await database.disconnect();
});
beforeEach(async () => {
// Limpiar base de datos antes de cada test
await database.clear();
});
});
Patrones de Testing Asíncrono
1. Async/Await (Recomendado)
describe('Patrón Async/Await', () => {
it('debería obtener datos de usuario', async () => {
const user = await fetchUser(123);
expect(user.name).to.equal('John');
});
it('debería manejar errores', async () => {
try {
await fetchUser(999);
expect.fail('Debería haber lanzado error');
} catch (error) {
expect(error.message).to.include('no encontrado');
}
});
});
2. Promesas
describe('Patrón Promise', () => {
it('debería obtener datos de usuario', () => {
return fetchUser(123).then(user => {
expect(user.name).to.equal('John');
});
});
it('debería manejar errores', () => {
return fetchUser(999).catch(error => {
expect(error.message).to.include('no encontrado');
});
});
});
Plugins de Chai
chai-http (Testing HTTP)
npm install --save-dev chai-http
const chai = require('chai');
const chaiHttp = require('chai-http');
const app = require('../src/app');
chai.use(chaiHttp);
const { expect } = chai;
describe('Endpoints de API', () => {
it('GET /api/users debería retornar usuarios', (done) => {
chai.request(app)
.get('/api/users')
.end((err, res) => {
expect(res).to.have.status(200);
expect(res.body).to.be.an('array');
expect(res.body).to.have.lengthOf.at.least(1);
done();
});
});
it('POST /api/users debería crear usuario', async () => {
const res = await chai.request(app)
.post('/api/users')
.send({ name: 'John', email: 'john@example.com' });
expect(res).to.have.status(201);
expect(res.body).to.have.property('id');
expect(res.body.name).to.equal('John');
});
});
chai-as-promised (Aserciones de Promesas)
npm install --save-dev chai-as-promised
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
const { expect } = chai;
describe('Testing de Promesas', () => {
it('debería resolver con datos de usuario', () => {
return expect(fetchUser(123)).to.eventually.have.property('name', 'John');
});
it('debería rechazar con error', () => {
return expect(fetchUser(999)).to.be.rejectedWith('Usuario no encontrado');
});
it('debería cumplirse', () => {
return expect(Promise.resolve('éxito')).to.be.fulfilled;
});
});
Reporters de Mocha
Reporters Integrados
# Spec (predeterminado) - vista jerárquica
mocha --reporter spec
# Matriz de puntos - salida mínima
mocha --reporter dot
# JSON - salida legible por máquina
mocha --reporter json > results.json
# HTML - salida amigable para navegador
mocha --reporter html > results.html
# TAP - Test Anything Protocol
mocha --reporter tap
Cobertura de Código con NYC
Instalación
npm install --save-dev nyc
Configuración (.nycrc.json)
{
"all": true,
"include": ["src/**/*.js"],
"exclude": ["test/**", "**/*.test.js"],
"reporter": ["html", "text", "lcov"],
"check-coverage": true,
"lines": 80,
"functions": 80,
"branches": 80,
"statements": 80
}
Ejecutar Cobertura
nyc mocha
# Con aplicación de umbral
nyc --check-coverage --lines 90 mocha
Mejores Prácticas
1. Nombres Descriptivos de Tests
// Mal
it('test 1', () => {});
// Bien
it('debería retornar 404 cuando el usuario no existe', () => {});
2. Patrón Arrange-Act-Assert
it('debería calcular precio total con descuento', () => {
// Arrange (Preparar)
const cart = new ShoppingCart();
cart.addItem({ price: 100 });
const discount = 0.1;
// Act (Actuar)
const total = cart.calculateTotal(discount);
// Assert (Afirmar)
expect(total).to.equal(90);
});
3. Una Aserción Por Test
// Evitar múltiples aserciones no relacionadas
it('debería validar usuario', () => {
expect(user.name).to.equal('John');
expect(user.age).to.equal(30);
expect(user.email).to.include('@');
});
// Mejor: tests separados
describe('Validación de Usuario', () => {
it('debería tener nombre correcto', () => {
expect(user.name).to.equal('John');
});
it('debería tener edad correcta', () => {
expect(user.age).to.equal(30);
});
it('debería tener email válido', () => {
expect(user.email).to.include('@');
});
});
4. Evitar Interdependencia de Tests
// Mal: tests dependen del orden de ejecución
describe('Mal Ejemplo', () => {
let userId;
it('debería crear usuario', async () => {
const user = await createUser({ name: 'John' });
userId = user.id; // Estado compartido
});
it('debería obtener usuario', async () => {
const user = await fetchUser(userId); // Depende del test anterior
expect(user.name).to.equal('John');
});
});
// Bien: tests independientes
describe('Buen Ejemplo', () => {
it('debería crear usuario', async () => {
const user = await createUser({ name: 'John' });
expect(user).to.have.property('id');
});
it('debería obtener usuario', async () => {
const created = await createUser({ name: 'John' });
const fetched = await fetchUser(created.id);
expect(fetched.name).to.equal('John');
});
});
Integración CI/CD
GitHub Actions
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Configurar Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Instalar dependencias
run: npm ci
- name: Ejecutar tests
run: npm test
- name: Generar cobertura
run: npm run test:coverage
- name: Subir cobertura a Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
Conclusión
Mocha y Chai proporcionan una base de testing poderosa y flexible para proyectos JavaScript. El diseño async-first de Mocha y su arquitectura flexible se combinan perfectamente con los estilos de aserción expresivos de Chai para crear tests legibles y mantenibles. Siguiendo las mejores prácticas—nombres descriptivos de tests, aislamiento apropiado de tests y cobertura comprehensiva—puedes construir una suite de tests robusta que detecta bugs temprano y sirve como documentación viva para tu código base.
Comienza con tests unitarios simples, agrega gradualmente tests de integración, aprovecha plugins para necesidades especializadas de testing, e integra reportes de cobertura para asegurar que tu aplicación permanezca confiable y mantenible.
Ver También
- Cypress Deep Dive - Framework moderno de testing end-to-end para JavaScript
- Playwright Framework Guide - Automatización de navegadores multiplataforma
- Estrategia de Automatización de Pruebas - Construir una estrategia efectiva de automatización
- Testing Continuo en DevOps - Integrar pruebas en pipelines CI/CD
- Pruebas de Rendimiento de API - Testing de rendimiento para APIs backend
Recursos Oficiales
FAQ
¿Cuál es la diferencia entre Mocha y Jest?
Mocha es un test runner flexible que requiere bibliotecas separadas para aserciones (Chai), mocking (Sinon) y cobertura (Istanbul). Jest es un framework todo-en-uno con aserciones, mocking y cobertura integrados. Mocha ofrece más flexibilidad; Jest ofrece setup más rápido y mejores defaults para React.
¿Cuáles son los tres estilos de aserciones de Chai?
- Assert: assert.equal(actual, expected). 2. Expect: expect(actual).to.equal(expected) — más legible, opción popular. 3. Should: actual.should.equal(expected) — legible pero puede fallar con null/undefined. El estilo expect se recomienda para la mayoría de proyectos.
¿Cómo pruebo código asíncrono con Mocha?
Mocha soporta tres patrones async: 1. Callbacks: parámetro done(). 2. Promises: retorna una Promise desde tu función de test. 3. Async/await: declara tu función de test con async y usa await. El patrón async/await se recomienda para código moderno.
¿Cómo configuro cobertura de código con Mocha?
Instala nyc (Istanbul): npm install –save-dev nyc. Añade en package.json: ’nyc mocha’. Configura en .nycrc: tipos de reporter (text, lcov, html), rutas include/exclude y umbrales de cobertura. Integra salida lcov con herramientas de reporting de CI como Codecov o Coveralls.
See Also
- Automatización BDD con Cucumber: Guía Completa de Pruebas Orientadas al Comportamiento
- Percy, Applitools & BackstopJS: Visual Regression Testing Solutions Compared - Comprehensive comparison of Percy, Applitools, and BackstopJS for visual…
- Guía completa de automatización BDD con Cucumber cubriendo…
- Locust Pruebas de Carga con Python: Guía Completa de Pruebas de Rendimiento - Guía comprehensiva de pruebas de carga con Locust cubriendo…
- K6: Load Testing Moderno con JavaScript para Equipos DevOps - Domina K6 para pruebas de rendimiento modernas: load testing…
- Katalon Studio: Plataforma Completa Todo-en-Uno para Automatización de Pruebas - Guía completa de Katalon Studio como solución todo-en-uno para…
