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

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?

  1. 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