Uso Práctico de Mocha y Chai para Pruebas en JavaScript: Tres Ejemplos Detallados

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.

Pruebas unitarias con Mocha y Chai

En desarrollo de software, escribir código que funcione es importante. Pero escribir código que podamos mantener, refactorizar y comprobar con confianza es todavía más importante. Ahí es donde entran en juego las pruebas unitarias, una práctica esencial para reducir errores, detectar regresiones y construir aplicaciones más sólidas.

Cuando trabajamos con JavaScript, existen muchas herramientas para crear tests: Jest, Vitest, Jasmine, el propio runner de Node.js y, por supuesto, la combinación formada por Mocha y Chai. Aunque han aparecido alternativas más recientes, Mocha y Chai siguen siendo una opción muy interesante por su flexibilidad, su claridad y su capacidad para adaptarse a distintos tipos de proyectos.

Mocha se encarga de organizar y ejecutar los tests. Chai, por su parte, nos ayuda a escribir las afirmaciones que comprueban si el resultado obtenido coincide con el resultado esperado. Dicho de forma sencilla: Mocha estructura la prueba y Chai expresa la expectativa.

Si ya has trabajado con JavaScript y quieres empezar a mejorar la calidad de tu código, este artículo te ayudará a entender cómo funcionan las pruebas unitarias con Mocha y Chai, cómo instalarlas, cómo escribir tus primeros tests y qué buenas prácticas conviene seguir para que las pruebas sean realmente útiles dentro de un proyecto.

Además, si te interesa seguir profundizando en testing, también puedes complementar esta lectura con el artículo sobre uso práctico de Mocha y Chai para pruebas en JavaScript, donde se trabajan ejemplos más concretos paso a paso.

Qué son las pruebas unitarias y por qué son importantes

Las pruebas unitarias son tests que verifican el comportamiento de una unidad pequeña de código de forma aislada. Esa unidad puede ser una función, un método, una clase, un helper o una pequeña pieza de lógica de negocio.

La idea principal es comprobar que una parte concreta del sistema hace exactamente lo que esperamos. Por ejemplo, si tenemos una función que calcula el precio final de un producto con IVA, una prueba unitaria debería validar que, al pasarle un precio base y un porcentaje de impuesto, el resultado sea correcto.

function calcularPrecioConIVA(precio, iva) {
  return precio + precio * iva;
}

Una prueba unitaria para esa función podría comprobar que, si el precio es 100 y el IVA es 0.21, el resultado sea 121.

La utilidad de este tipo de pruebas no está solo en encontrar errores. Su verdadero valor aparece cuando el proyecto crece. Si modificamos una función, cambiamos una dependencia o reorganizamos parte del código, los tests nos ayudan a comprobar rápidamente si algo que antes funcionaba ha dejado de hacerlo.

En otras palabras, las pruebas unitarias actúan como una red de seguridad. No eliminan todos los errores, pero sí reducen mucho el riesgo de romper funcionalidades existentes sin darnos cuenta.

Ventajas de aplicar pruebas unitarias

Implementar pruebas unitarias con Mocha y Chai puede aportar beneficios muy claros:

  • Permiten detectar errores de forma temprana.
  • Facilitan la refactorización del código.
  • Ayudan a documentar el comportamiento esperado de una función.
  • Mejoran la confianza al hacer cambios.
  • Reducen el coste de mantenimiento a largo plazo.
  • Favorecen una arquitectura más modular y desacoplada.

Además, escribir tests obliga a pensar mejor la estructura del código. Si una función es muy difícil de probar, normalmente es una señal de que hace demasiadas cosas o depende demasiado de elementos externos.

Esto conecta directamente con una idea clave del desarrollo frontend moderno: cuanto más clara es la responsabilidad de cada pieza, más fácil resulta mantenerla, probarla y escalarla. Lo mismo ocurre cuando trabajamos con componentes, estados o utilidades en proyectos con frameworks como React, algo que también puedes ver en contenidos relacionados como pruebas en React y React Testing Library.

Qué es Mocha

Mocha es un framework de testing para JavaScript que permite definir, organizar y ejecutar pruebas. Se puede utilizar en proyectos Node.js y también en entornos de navegador.

Una de sus principales ventajas es que no impone una única forma de trabajar. Mocha proporciona la estructura de ejecución, pero nos deja elegir otras herramientas para las aserciones, los mocks, los stubs o la cobertura de código.

Por eso se suele decir que Mocha es una herramienta flexible. No viene con todas las decisiones tomadas de antemano, lo que puede ser una ventaja en proyectos donde necesitamos adaptar el entorno de testing a nuestras necesidades.

Cómo organiza Mocha los tests

Mocha utiliza una sintaxis muy legible basada en bloques como describe() e it().

describe("calcularPrecioConIVA", function () {
  it("debería calcular el precio final con IVA", function () {
    // prueba aquí
  });
});

El bloque describe() sirve para agrupar pruebas relacionadas. Normalmente se utiliza para indicar qué función, módulo o comportamiento estamos probando.

El bloque it() describe un caso concreto. Lo ideal es que su texto explique claramente qué debería ocurrir.

it("debería devolver 121 cuando el precio es 100 y el IVA es 21%", function () {
  // prueba aquí
});

Esta forma de escribir tests tiene una ventaja importante: el propio test se convierte en una pequeña documentación del comportamiento esperado del código.

Cuando alguien del equipo lee el test, puede entender qué hace esa función sin tener que revisar toda la implementación interna. Y cuando ese test falla, el mensaje describe con bastante precisión qué comportamiento ha dejado de cumplirse.

Hooks en Mocha

Mocha también permite utilizar hooks para preparar o limpiar el entorno antes y después de los tests. Los más habituales son:

before(function () {
  // Se ejecuta una vez antes de todos los tests
});

after(function () {
  // Se ejecuta una vez después de todos los tests
});

beforeEach(function () {
  // Se ejecuta antes de cada test
});

afterEach(function () {
  // Se ejecuta después de cada test
});

Estos hooks son útiles cuando necesitamos inicializar datos, crear una instancia, limpiar variables o preparar un estado común para varios tests.

Aun así, conviene usarlos con cuidado. Si abusamos de ellos, los tests pueden volverse difíciles de leer porque parte de la lógica queda escondida fuera del caso de prueba. Como norma general, un test debería poder entenderse sin tener que saltar constantemente entre varias partes del archivo.

Qué es Chai

Chai es una librería de aserciones para JavaScript. Su función es ayudarnos a expresar de forma clara qué esperamos que ocurra en una prueba.

Mientras Mocha ejecuta y organiza los tests, Chai permite escribir afirmaciones como esta:

expect(resultado).to.equal(121);

Esta línea significa: “espero que el resultado sea igual a 121”.

Chai puede utilizarse con diferentes estilos de aserción. Los más conocidos son:

  • expect
  • should
  • assert

En muchos proyectos modernos se utiliza expect porque resulta muy expresivo y fácil de leer.

Ejemplo con expect

import { expect } from "chai";

expect(2 + 2).to.equal(4);
expect("Mocha").to.be.a("string");
expect([1, 2, 3]).to.include(2);

La sintaxis de Chai está pensada para que las pruebas se puedan leer casi como frases. Esto ayuda a que el test sea más comprensible, incluso para personas que no escribieron originalmente ese código.

Chai y la legibilidad de las pruebas

Una buena prueba no solo debe funcionar. También debe ser fácil de entender.

Por eso Chai aporta mucho valor: permite escribir expectativas de forma más cercana al lenguaje natural. No es lo mismo leer esto:

assert.equal(resultado, 121);

que esto:

expect(resultado).to.equal(121);

Ambas opciones son válidas, pero expect suele resultar más expresiva cuando queremos construir tests claros y descriptivos.

La legibilidad no es un detalle menor. En proyectos reales, los tests también forman parte del código base. Por tanto, deben mantenerse con el mismo cuidado que el resto de archivos del proyecto.

Mocha y Chai juntos: una combinación sencilla y potente

Mocha y Chai se complementan muy bien porque cada herramienta se centra en una responsabilidad distinta.

Mocha responde a preguntas como:

  • ¿Dónde están los tests?
  • ¿Cómo se agrupan?
  • ¿Qué tests se ejecutan?
  • ¿Qué tests pasan y cuáles fallan?
  • ¿Cómo se muestran los resultados?

Chai responde a preguntas como:

  • ¿Qué valor esperaba?
  • ¿El resultado es igual al previsto?
  • ¿El array contiene determinado elemento?
  • ¿El objeto tiene cierta propiedad?
  • ¿La función lanza un error?

Esta separación hace que el entorno de pruebas sea más modular. Podemos usar Mocha con Chai, pero también podríamos combinar Mocha con otras librerías de aserciones. Del mismo modo, Chai puede integrarse con otros frameworks de testing.

Cuándo conviene usar Mocha y Chai

La combinación de Mocha y Chai es especialmente útil cuando queremos un entorno de testing claro, configurable y progresivo.

Puede ser una buena elección en estos casos:

  • Proyectos Node.js con lógica de negocio.
  • Librerías JavaScript reutilizables.
  • APIs donde queremos probar funciones, servicios o validadores.
  • Proyectos donde necesitamos una configuración flexible.
  • Equipos que prefieren separar framework de testing y librería de aserciones.

No siempre es necesario usar Mocha y Chai. En algunos proyectos modernos con Vite, por ejemplo, Vitest puede integrarse de forma muy cómoda. Pero Mocha y Chai siguen siendo una excelente opción cuando buscamos control, estabilidad y una sintaxis muy reconocible.

Esta decisión también depende del contexto técnico del proyecto. Si estás trabajando en una aplicación frontend moderna, puede interesarte revisar primero cómo está organizado tu stack, tus scripts de desarrollo y tus herramientas de automatización. En ese sentido, también puede resultarte útil el artículo sobre GitHub Actions, automatización e integración continua en desarrollo de software.

Cómo instalar Mocha y Chai en un proyecto JavaScript

Para empezar, necesitamos tener un proyecto Node.js inicializado. Si todavía no lo tenemos, podemos crearlo con:

npm init -y

Después instalamos Mocha y Chai como dependencias de desarrollo:

npm install --save-dev mocha chai

Una vez instaladas las dependencias, podemos crear una estructura básica como esta:

mi-proyecto/
├── src/
│   └── calculadora.js
├── test/
│   └── calculadora.test.js
├── package.json

En el archivo package.json, podemos añadir un script para ejecutar los tests:

{
  "scripts": {
    "test": "mocha"
  }
}

De esta forma, podremos lanzar las pruebas con:

npm test

Configuración con módulos ES

Si queremos utilizar import y export, podemos añadir "type": "module" en el package.json:

{
  "type": "module",
  "scripts": {
    "test": "mocha"
  },
  "devDependencies": {
    "chai": "^6.0.0",
    "mocha": "^11.0.0"
  }
}

Las versiones concretas pueden variar según el momento en que instales los paquetes, pero lo importante es entender la estructura general.

Si estás empezando también con tipado estático, te puede interesar complementar esta parte con una introducción a TypeScript y sus primeros pasos, ya que muchos proyectos actuales combinan tests con JavaScript moderno o TypeScript.

Primer ejemplo de prueba unitaria con Mocha y Chai

Imaginemos que tenemos una función sencilla para sumar dos números.

Archivo src/calculadora.js:

export function sumar(a, b) {
  return a + b;
}

Ahora creamos el archivo de prueba.

Archivo test/calculadora.test.js:

import { expect } from "chai";
import { sumar } from "../src/calculadora.js";

describe("sumar", function () {
  it("debería sumar dos números positivos", function () {
    const resultado = sumar(2, 3);

    expect(resultado).to.equal(5);
  });
});

Este test tiene tres partes claras:

  1. Importamos la función que queremos probar.
  2. Ejecutamos la función con unos valores concretos.
  3. Comprobamos que el resultado sea el esperado.

Si ejecutamos:

npm test

Mocha buscará los tests, los ejecutará y mostrará si han pasado o fallado.

Probando diferentes escenarios

Una sola prueba no siempre es suficiente. Para que una prueba unitaria sea útil, debemos pensar en distintos escenarios.

import { expect } from "chai";
import { sumar } from "../src/calculadora.js";

describe("sumar", function () {
  it("debería sumar dos números positivos", function () {
    expect(sumar(2, 3)).to.equal(5);
  });

  it("debería sumar números negativos", function () {
    expect(sumar(-2, -3)).to.equal(-5);
  });

  it("debería sumar un número positivo y uno negativo", function () {
    expect(sumar(10, -4)).to.equal(6);
  });

  it("debería devolver el mismo número al sumar cero", function () {
    expect(sumar(7, 0)).to.equal(7);
  });
});

Este ejemplo es muy simple, pero muestra una idea importante: no probamos solo el caso feliz. También conviene probar límites, casos especiales y combinaciones que podrían generar errores.

En desarrollo real, muchos bugs aparecen precisamente en los bordes: datos vacíos, valores inesperados, estados intermedios o entradas que no habíamos contemplado. Por eso, un buen conjunto de pruebas debe ir más allá del escenario ideal.

Ejemplo práctico: validar un carrito de compra

Veamos un caso un poco más cercano a un proyecto real. Imaginemos una función que calcula el total de un carrito.

Archivo src/carrito.js:

export function calcularTotal(productos) {
  return productos.reduce((total, producto) => {
    return total + producto.precio * producto.cantidad;
  }, 0);
}

Ahora escribimos los tests.

Archivo test/carrito.test.js:

import { expect } from "chai";
import { calcularTotal } from "../src/carrito.js";

describe("calcularTotal", function () {
  it("debería devolver 0 si el carrito está vacío", function () {
    const resultado = calcularTotal([]);

    expect(resultado).to.equal(0);
  });

  it("debería calcular el total de un producto", function () {
    const productos = [
      { nombre: "Curso JavaScript", precio: 30, cantidad: 1 }
    ];

    const resultado = calcularTotal(productos);

    expect(resultado).to.equal(30);
  });

  it("debería calcular el total de varios productos con distintas cantidades", function () {
    const productos = [
      { nombre: "Libro", precio: 20, cantidad: 2 },
      { nombre: "Plantilla", precio: 15, cantidad: 3 }
    ];

    const resultado = calcularTotal(productos);

    expect(resultado).to.equal(85);
  });
});

Este tipo de prueba resulta muy útil porque valida una regla de negocio concreta: cómo se calcula el total de un carrito.

Si más adelante modificamos la función para añadir descuentos, impuestos o gastos de envío, los tests nos ayudarán a saber si hemos roto el cálculo original.

Aquí se ve muy bien una de las grandes ventajas de las pruebas unitarias: nos permiten cambiar el código con más tranquilidad. No porque garanticen que todo sea perfecto, sino porque nos avisan cuando una pieza importante deja de comportarse como esperábamos.

Pruebas unitarias de errores y validaciones

En muchos casos, una función no solo debe devolver un resultado correcto. También debe gestionar errores o entradas inválidas.

Imaginemos una función que divide dos números:

export function dividir(a, b) {
  if (b === 0) {
    throw new Error("No se puede dividir entre cero");
  }

  return a / b;
}

Podemos probar tanto el caso correcto como el error:

import { expect } from "chai";
import { dividir } from "../src/dividir.js";

describe("dividir", function () {
  it("debería dividir dos números", function () {
    expect(dividir(10, 2)).to.equal(5);
  });

  it("debería lanzar un error si se divide entre cero", function () {
    expect(() => dividir(10, 0)).to.throw("No se puede dividir entre cero");
  });
});

Este tipo de prueba es importante porque valida el comportamiento completo de la función, no solo el resultado ideal.

Una aplicación robusta no es la que nunca recibe datos incorrectos, sino la que sabe responder bien cuando esos datos aparecen.

La importancia de probar entradas inválidas

En proyectos reales, no siempre recibimos datos limpios. Puede llegar un array vacío, un objeto incompleto, una cadena donde esperábamos un número o una respuesta externa con una estructura distinta a la prevista.

Por eso, cuando escribimos pruebas unitarias, conviene preguntarnos:

  • ¿Qué ocurre si falta un dato?
  • ¿Qué ocurre si el valor es null o undefined?
  • ¿Qué ocurre si el array está vacío?
  • ¿Qué ocurre si recibimos un tipo de dato inesperado?
  • ¿Qué error debería lanzarse en ese caso?

Estas preguntas ayudan a diseñar funciones más resistentes y fáciles de mantener.

Pruebas asíncronas con Mocha y Chai

JavaScript trabaja constantemente con código asíncrono: peticiones HTTP, lectura de archivos, consultas a bases de datos, temporizadores o promesas.

Mocha permite probar código asíncrono de varias formas. Una de las más cómodas es utilizando async y await.

Imaginemos esta función:

export async function obtenerUsuario(id) {
  if (!id) {
    throw new Error("El id es obligatorio");
  }

  return {
    id,
    nombre: "Marta"
  };
}

Podemos probarla así:

import { expect } from "chai";
import { obtenerUsuario } from "../src/usuario.js";

describe("obtenerUsuario", function () {
  it("debería devolver un usuario por id", async function () {
    const usuario = await obtenerUsuario(1);

    expect(usuario).to.deep.equal({
      id: 1,
      nombre: "Marta"
    });
  });
});

En este caso usamos deep.equal porque estamos comparando objetos. Si utilizáramos equal, Chai comprobaría si ambos objetos son exactamente la misma referencia en memoria, no si tienen el mismo contenido.

Cuidado con los tests asíncronos

Uno de los errores más habituales al probar código asíncrono es olvidar devolver la promesa o usar await.

Este test puede parecer correcto, pero no lo es:

it("debería devolver un usuario", function () {
  obtenerUsuario(1).then((usuario) => {
    expect(usuario.nombre).to.equal("Marta");
  });
});

El problema es que Mocha podría terminar el test antes de que la promesa se resuelva. Por eso, en la mayoría de casos, es preferible utilizar async/await:

it("debería devolver un usuario", async function () {
  const usuario = await obtenerUsuario(1);

  expect(usuario.nombre).to.equal("Marta");
});

La legibilidad mejora y el riesgo de falsos positivos se reduce.

Si además estás probando lógica relacionada con almacenamiento local, sesiones o persistencia en navegador, puedes complementar esta parte con el artículo sobre localStorage y sessionStorage en proyectos JavaScript, ya que muchas funciones de ese tipo también pueden beneficiarse de pruebas bien planteadas.

Buenas prácticas para escribir pruebas unitarias con Mocha y Chai

Escribir tests no consiste en llenar el proyecto de archivos .test.js. La calidad de una prueba depende de su claridad, su utilidad y su capacidad para detectar errores reales.

Escribe tests pequeños y específicos

Cada test debería comprobar una idea concreta. Si un test valida demasiadas cosas a la vez, será más difícil saber qué ha fallado cuando aparezca un error.

Un buen test debería poder responder con claridad a esta pregunta: ¿qué comportamiento exacto estoy verificando?

Por ejemplo, no es lo mismo comprobar “la función de carrito funciona” que comprobar “la función devuelve 0 cuando el carrito está vacío”. La segunda formulación es mucho más concreta y ayuda a detectar el origen del problema con mayor rapidez.

Usa nombres descriptivos

El texto dentro de it() debe explicar el comportamiento esperado.

Mejor esto:

it("debería devolver 0 cuando el carrito está vacío", function () {});

que esto:

it("test 1", function () {});

Los nombres descriptivos hacen que los resultados de Mocha sean mucho más útiles. Cuando una prueba falla, el propio nombre del test nos orienta sobre el problema.

Evita depender de otros tests

Una prueba unitaria debería poder ejecutarse de forma independiente. Si un test depende de que otro se haya ejecutado antes, el conjunto de pruebas se vuelve frágil.

Cada test debe preparar sus propios datos o utilizar hooks claros como beforeEach() cuando sea necesario.

La independencia de los tests es clave para evitar errores difíciles de rastrear. Si una prueba falla solo cuando se ejecuta después de otra, probablemente hay un problema de estado compartido.

Prueba casos límite

No te quedes solo con el caso más evidente. Algunas de las situaciones más valiosas para probar son:

  • Valores vacíos.
  • Valores nulos o indefinidos.
  • Números negativos.
  • Arrays vacíos.
  • Objetos incompletos.
  • Errores esperados.
  • Límites máximos y mínimos.

Los bugs suelen esconderse en los bordes, no en el camino perfecto.

No pruebes detalles de implementación

Una buena prueba se centra en el comportamiento, no en cómo está construido internamente el código.

Por ejemplo, si una función devuelve el resultado correcto, normalmente no debería importarnos si internamente usa map, reduce o un bucle for.

Probar demasiados detalles internos hace que los tests se rompan cada vez que refactorizamos, incluso aunque el comportamiento final siga siendo correcto.

La pregunta clave debería ser: ¿el código sigue haciendo lo que debe hacer? Si la respuesta es sí, el test no debería fallar solo porque hemos mejorado su implementación interna.

Errores comunes al empezar con Mocha y Chai

Aunque Mocha y Chai son herramientas bastante accesibles, es normal cometer algunos errores al principio.

Escribir tests demasiado genéricos

Un test como este aporta poco:

it("debería funcionar", function () {});

Conviene explicar qué significa “funcionar”. Los tests deben ser una guía, no un misterio.

Un buen test debería dejar claro qué entrada se está probando, qué resultado se espera y qué comportamiento se está validando.

No probar errores

Muchas veces se prueban solo los resultados correctos, pero no las situaciones problemáticas. Validar errores también forma parte del comportamiento esperado de una función.

Por ejemplo, si una función debe lanzar un error cuando falta un parámetro obligatorio, ese comportamiento también debería tener su propio test.

Mezclar demasiada lógica dentro del test

Si dentro del test hay demasiados cálculos, condicionales o transformaciones, quizá el test se está volviendo más complejo que el propio código probado.

Un test debe ser simple: preparar datos, ejecutar la función y comprobar el resultado.

Una estructura muy habitual es la conocida como Arrange, Act, Assert:

  • Preparar los datos.
  • Ejecutar la acción.
  • Comprobar el resultado.

Aunque no es obligatorio seguirla siempre de forma estricta, puede ayudar mucho a mantener los tests ordenados.

Abusar de mocks innecesarios

Los mocks pueden ser útiles, sobre todo cuando hay dependencias externas. Pero si se usan sin necesidad, pueden hacer que las pruebas sean más difíciles de mantener.

En pruebas unitarias, lo ideal es aislar la lógica, pero sin convertir cada test en una simulación artificial imposible de entender.

Cómo integrar las pruebas unitarias en el flujo de trabajo

Las pruebas unitarias son más útiles cuando forman parte del día a día del proyecto.

Lo más recomendable es ejecutar los tests:

  • Antes de hacer un commit importante.
  • Antes de abrir una pull request.
  • Después de refactorizar una función.
  • Antes de desplegar a producción.
  • Dentro de un flujo de integración continua.

Por ejemplo, en un proyecto con GitHub Actions podríamos configurar un workflow para ejecutar npm test automáticamente cada vez que se suben cambios al repositorio.

Esto permite detectar errores antes de que lleguen a producción y ayuda a mantener un estándar mínimo de calidad en el código.

Si estás trabajando en proyectos que se despliegan en repositorios públicos o privados, también puedes ampliar esta parte con una guía sobre desplegar proyectos en GitHub Pages, especialmente si quieres conectar testing, automatización y publicación.

Testing e integración continua

Una de las mejores formas de sacar partido a las pruebas unitarias es integrarlas en un proceso de integración continua.

Esto significa que los tests se ejecutan automáticamente cuando se suben cambios al repositorio. Así, si una modificación rompe una funcionalidad existente, el equipo puede detectarlo antes de fusionar el código o desplegarlo.

En proyectos pequeños puede parecer innecesario, pero a medida que el código crece, automatizar las pruebas se convierte en una ayuda enorme.

No se trata solo de ahorrar tiempo. También se trata de reducir la incertidumbre.

Mocha y Chai frente a otras herramientas de testing

Es normal preguntarse si merece la pena aprender Mocha y Chai cuando existen alternativas más recientes.

La respuesta depende del proyecto.

Mocha y Chai destacan por su flexibilidad. No intentan resolverlo todo de una sola vez, sino que permiten construir un entorno de testing a medida. Esto puede ser ideal para proyectos Node.js, librerías o aplicaciones donde queremos controlar cada pieza del ecosistema.

Herramientas como Jest o Vitest suelen ofrecer una experiencia más integrada, con aserciones, mocks y cobertura en un solo paquete. Esto puede ser muy cómodo, especialmente en proyectos frontend modernos.

Sin embargo, aprender Mocha y Chai sigue siendo muy útil porque ayuda a comprender mejor los fundamentos del testing en JavaScript: estructura de pruebas, aserciones, casos límite, pruebas asíncronas y separación de responsabilidades.

Entonces, ¿cuál debería elegir?

Si estás empezando desde cero y quieres entender bien las bases, Mocha y Chai son una combinación muy didáctica.

Si trabajas en un proyecto ya configurado con Jest o Vitest, probablemente tenga sentido seguir con la herramienta existente para mantener coherencia dentro del equipo.

Lo importante no es elegir la herramienta más popular, sino crear una cultura de pruebas que ayude a mejorar el código y reducir errores.

FAQs sobre pruebas unitarias con Mocha y Chai

¿Mocha y Chai sirven solo para Node.js?

No. Mocha puede ejecutarse tanto en Node.js como en navegador, y Chai también puede utilizarse en ambos entornos. Aun así, su uso más habitual suele estar en proyectos JavaScript y Node.js, especialmente para probar lógica de negocio, utilidades, servicios y módulos independientes.

¿Cuál es la diferencia entre Mocha y Chai?

Mocha es el framework que organiza y ejecuta las pruebas. Chai es la librería que permite escribir las aserciones. Dicho de otra forma, Mocha define la estructura del test y Chai comprueba si el resultado obtenido coincide con el resultado esperado.

Por eso se utilizan tan bien juntos: cada herramienta cumple una función clara.

¿Es mejor usar Mocha y Chai o Jest?

No hay una respuesta universal. Jest ofrece una experiencia más integrada y suele utilizarse mucho en proyectos frontend, especialmente con React. Mocha y Chai ofrecen más flexibilidad y permiten configurar el entorno de testing de forma más modular.

Si buscas una solución todo en uno, Jest o Vitest pueden ser opciones cómodas. Si prefieres controlar mejor cada parte del entorno de pruebas, Mocha y Chai siguen siendo una combinación muy sólida.

Probar con intención: la base de un código más confiable

Las pruebas unitarias con Mocha y Chai no son solo una práctica técnica. También son una forma de trabajar con más claridad, más seguridad y menos miedo a cambiar el código.

Cuando un proyecto no tiene tests, cada modificación puede generar dudas: “¿habré roto algo?”, “¿seguirá funcionando este cálculo?”, “¿qué pasa si cambio esta función?”. En cambio, cuando contamos con una buena base de pruebas unitarias, el código se vuelve más fácil de mantener y evolucionar.

Mocha y Chai ofrecen una combinación sencilla, flexible y potente para empezar a escribir tests en JavaScript. Mocha nos da la estructura. Chai nos da la expresividad. Juntas, ambas herramientas nos permiten construir pruebas claras, legibles y enfocadas en el comportamiento real del código.

La clave no está en probarlo absolutamente todo, sino en probar lo importante: las reglas de negocio, los casos límite, los errores esperados y aquellas funciones que sostienen partes críticas de la aplicación.

En desarrollo web, la calidad no aparece al final del proceso. Se construye poco a poco, decisión a decisión. Y escribir buenas pruebas unitarias es una de esas decisiones que, aunque al principio parezca pequeña, puede marcar una gran diferencia en la vida útil de un proyecto.

Cómo el Efecto Patito de Goma Optimiza el Desarrollo de Aplicaciones

En programación, no siempre encontramos los errores mirando más tiempo la pantalla. A veces, el problema no está en que falte conocimiento técnico, sino en que tenemos demasiadas ideas mezcladas en la cabeza: una función que no responde como esperábamos, una condición que parece correcta pero no se cumple, una arquitectura que empieza a volverse confusa o un bug que aparece justo cuando pensábamos que todo estaba controlado.

En ese contexto aparece una técnica tan sencilla como efectiva: el Efecto Patito de Goma, también conocido como Rubber Duck Debugging. Su propuesta es simple: explicar en voz alta el problema, el código o la lógica que estamos siguiendo, como si se lo contáramos a un patito de goma colocado sobre el escritorio.

Puede sonar curioso, incluso un poco absurdo al principio. Sin embargo, esta práctica se ha convertido en una herramienta muy útil dentro del desarrollo de aplicaciones, porque ayuda a ordenar el pensamiento, detectar errores ocultos y mejorar la manera en la que razonamos frente al código.

El Efecto Patito de Goma no sustituye a las pruebas, a las revisiones de código ni a las buenas prácticas de programación. Pero sí puede convertirse en un recurso muy valioso para desbloquear problemas, especialmente cuando llevamos demasiado tiempo atrapados en la misma idea.

Qué es el Efecto Patito de Goma

El Efecto Patito de Goma es una técnica de resolución de problemas en programación que consiste en explicar paso a paso el código o el problema que queremos resolver a un objeto inanimado, normalmente representado por un patito de goma.

La idea es que, al verbalizar lo que estamos haciendo, nuestro cerebro se ve obligado a organizar la información de forma más clara. No basta con pensar “esto debería funcionar”. Tenemos que explicar qué esperamos que ocurra, qué está ocurriendo realmente y qué parte del proceso puede estar fallando.

Cuando convertimos un problema en una explicación, dejamos de verlo como una masa confusa de líneas de código y empezamos a descomponerlo en partes más pequeñas. Y esa descomposición suele ser el primer paso para encontrar la solución.

De dónde viene el término Rubber Duck Debugging

El término Rubber Duck Debugging se popularizó a partir de una anécdota mencionada en el libro The Pragmatic Programmer, de Andrew Hunt y David Thomas. En ella se describe a un programador que llevaba un patito de goma y le explicaba su código línea por línea para detectar errores.

Más allá de la anécdota, el concepto refleja una realidad muy común en el desarrollo de software: muchas veces encontramos la respuesta justo cuando intentamos explicarle el problema a otra persona. De hecho, es habitual que alguien pida ayuda a un compañero, empiece a contarle lo que ocurre y, antes de que la otra persona diga nada, se dé cuenta del error.

El patito de goma cumple ese mismo papel, pero sin interrumpir, sin juzgar y sin necesidad de tener disponibilidad en ese momento.

Por qué funciona esta técnica

El Efecto Patito de Goma funciona porque obliga a transformar un pensamiento interno, rápido y desordenado, en una explicación externa, lenta y estructurada.

Cuando programamos, damos muchas cosas por supuestas. Sabemos lo que “queríamos hacer”, pero no siempre revisamos si el código realmente hace eso. Al explicar el proceso en voz alta, aparecen contradicciones, huecos lógicos y pasos que antes parecían evidentes, pero que no estaban tan claros.

Por ejemplo, puedes pensar:

“Esta función debería guardar los datos del formulario.”

Pero cuando empiezas a explicarla, quizás dices:

“Primero recojo los datos del formulario, después valido los campos, luego llamo a la API y finalmente actualizo el estado…”

Y justo ahí te das cuenta de que la validación está devolviendo false, de que el estado se actualiza antes de tiempo o de que la llamada a la API nunca llega a ejecutarse.

Ese es el valor real de la técnica: te obliga a mirar el código desde fuera.

Cómo ayuda el Efecto Patito de Goma a depurar código

Depurar código no consiste únicamente en buscar errores visibles. Muchas veces implica revisar decisiones, suposiciones y relaciones entre distintas partes de una aplicación. Por eso el Rubber Duck Debugging puede ser tan útil.

Ayuda a ralentizar el pensamiento

Cuando estamos bloqueados, solemos saltar rápidamente de una hipótesis a otra. Probamos un cambio, recargamos la página, añadimos un console.log, modificamos una condición y volvemos a intentarlo. A veces funciona, pero otras veces solo aumentamos la confusión.

Explicar el problema en voz alta obliga a ir más despacio. Y en programación, ir más despacio no siempre significa perder tiempo. Muchas veces significa evitar cambios impulsivos que generan más errores de los que resuelven.

El patito de goma nos obliga a seguir una secuencia:

  • Qué esperaba que pasara.
  • Qué está pasando realmente.
  • Qué parte del código interviene.
  • Qué datos entran.
  • Qué datos salen.
  • En qué punto aparece el comportamiento inesperado.

Ese orden reduce el ruido mental y facilita una depuración más precisa.

Además, esta forma de trabajar encaja muy bien con otros hábitos de productividad para desarrolladores, como preparar mejor el entorno de trabajo, usar bien la terminal o apoyarse en herramientas del editor. Si te interesa mejorar ese flujo diario, también puedes leer esta guía sobre atajos y trucos para usar Visual Studio Code desde la terminal en Mac.

Mejora la lógica de programación

La lógica de programación no solo se entrena escribiendo código. También se entrena explicándolo.

Cuando una persona desarrolladora intenta explicar una función, una condición o un flujo de datos, necesita comprobar si las relaciones entre las partes tienen sentido. Si no puede explicarlo con claridad, probablemente el código también necesite revisión.

Esto resulta especialmente útil en casos como:

  • condiciones complejas;
  • funciones con demasiadas responsabilidades;
  • componentes difíciles de entender;
  • flujos asíncronos;
  • errores que dependen del estado de la aplicación;
  • validaciones de formularios;
  • llamadas a APIs;
  • problemas de renderizado en interfaces.

El Efecto Patito de Goma permite comprobar si el razonamiento detrás del código es coherente antes incluso de cambiar una sola línea.

Detecta supuestos invisibles

Uno de los mayores enemigos del debugging son los supuestos.

Damos por hecho que una variable tiene un valor concreto. Damos por hecho que una función se ejecuta. Damos por hecho que el usuario sigue un flujo determinado. Damos por hecho que el backend devuelve la respuesta esperada.

Pero el código no funciona según lo que suponemos. Funciona según lo que realmente está escrito.

Al explicar el problema en voz alta, esos supuestos salen a la luz. Es muy frecuente descubrir frases como:

“Esta variable siempre debería tener datos…”

“Este evento debería dispararse al hacer clic…”

“Esta función debería ejecutarse después de guardar…”

La palabra “debería” suele ser una señal importante. Indica que hay una hipótesis que necesita comprobarse.

Ejemplo sencillo

Imagina que tienes un botón para enviar un formulario, pero al hacer clic no ocurre nada.

Puedes empezar explicándolo así:

“El usuario completa el formulario. Después pulsa el botón de enviar. Al hacer clic, debería ejecutarse la función handleSubmit. Esa función valida los campos y, si todo está correcto, envía los datos.”

Al decirlo en voz alta, revisas el botón y descubres que no tiene asociado el evento onClick, que el botón está fuera del formulario o que la función se llama diferente.

El error no apareció porque el patito supiera programar. Apareció porque explicar el flujo te obligó a revisar cada paso.

Cómo aplicar el Rubber Duck Debugging paso a paso

Aunque la técnica parece muy simple, puede aplicarse de manera más efectiva si seguimos una pequeña estructura. No se trata solo de hablar por hablar, sino de convertir la explicación en una herramienta de análisis.

Paso 1: define el problema con claridad

Antes de mirar el código, intenta formular el problema en una frase concreta.

No es lo mismo decir:

“Esto no funciona.”

Que decir:

“El formulario se envía, pero el mensaje de éxito no aparece después de recibir la respuesta de la API.”

La segunda frase es mucho más útil porque delimita el problema. Ya no estás analizando toda la aplicación, sino una parte específica del flujo.

Paso 2: explica qué esperabas que ocurriera

El siguiente paso es contarle al patito cuál era el comportamiento esperado.

Por ejemplo:

“Cuando el usuario pulsa el botón, esperaba que se validaran los campos, se enviaran los datos y apareciera un mensaje de confirmación.”

Esto ayuda a separar la intención del resultado. En desarrollo de aplicaciones, esa diferencia es fundamental.

Paso 3: describe qué ocurre realmente

Después, explica el comportamiento actual.

“El usuario pulsa el botón, los datos parecen enviarse, pero no aparece ningún mensaje. Además, en consola no hay errores.”

Esta parte es importante porque evita que trabajes solo con sensaciones. Cuanto más específico sea el comportamiento observado, más fácil será localizar el fallo.

Paso 4: recorre el código línea por línea

Aquí es donde el Efecto Patito de Goma resulta más potente. Lee o explica el código como si tuvieras que enseñárselo a alguien que no conoce el proyecto.

Puedes usar frases como:

“Esta función recibe los datos del formulario.”

“Después comprueba si el campo email está vacío.”

“Si hay errores, devuelve el objeto de errores.”

“Si no hay errores, llama a esta función para enviar los datos.”

Al hacerlo, es probable que encuentres incoherencias entre lo que dices y lo que el código realmente hace.

Paso 5: identifica el punto exacto de ruptura

El objetivo no es arreglar todo de golpe, sino encontrar dónde se rompe el flujo.

Puede estar en la entrada de datos, en una condición, en una promesa, en una llamada externa, en el estado del componente o en una respuesta inesperada.

Una vez localizado el punto, la solución suele ser mucho más evidente.

Beneficios del Efecto Patito de Goma en el desarrollo de aplicaciones

El Efecto Patito de Goma no solo sirve para resolver bugs puntuales. También puede mejorar la forma en la que una persona desarrolladora piensa, comunica y toma decisiones técnicas.

Favorece buenas prácticas de programación

Explicar el código ayuda a detectar funciones demasiado largas, nombres poco claros o estructuras difíciles de mantener. Si necesitas dar muchas vueltas para explicar qué hace una función, quizás esa función está haciendo demasiadas cosas.

En ese sentido, el patito de goma también puede ser una herramienta indirecta de refactorización. No cambia el código por ti, pero te muestra cuándo algo no se entiende bien.

Un código difícil de explicar suele ser un código difícil de mantener.

Reduce la dependencia inmediata de otras personas

Pedir ayuda al equipo es positivo y necesario, pero no siempre conviene hacerlo como primer recurso. Muchas veces podemos llegar a una mejor pregunta si antes hemos intentado explicar el problema por nuestra cuenta.

El Rubber Duck Debugging permite llegar a una conversación técnica con más claridad. En lugar de decir “no funciona”, puedes decir:

“He comprobado que la función se ejecuta, que los datos llegan correctamente, pero la respuesta no actualiza el estado como esperaba.”

Esa diferencia mejora mucho la calidad de la colaboración.

Mejora la comunicación técnica

Una de las habilidades más importantes en programación es saber explicar decisiones técnicas. No basta con escribir código: también hay que justificarlo, documentarlo, revisarlo y compartirlo con otras personas.

Practicar el Efecto Patito de Goma mejora esa capacidad porque entrena la explicación clara. Y una persona que sabe explicar bien su código suele colaborar mejor en revisiones, reuniones técnicas y procesos de documentación.

Esta idea conecta también con otros conceptos de experiencia de usuario y aprendizaje. Por ejemplo, aunque no es lo mismo, puede resultar interesante diferenciarlo del síndrome Baby Duck en UX, que habla de cómo las primeras experiencias con una interfaz pueden condicionar nuestras preferencias futuras.

Aumenta la productividad para desarrolladores

Aunque pueda parecer que hablar con un patito de goma ralentiza el trabajo, en realidad puede ahorrar mucho tiempo. Evita búsquedas desordenadas, cambios al azar y bloqueos prolongados.

La productividad para desarrolladores no consiste en escribir código sin parar, sino en resolver problemas con menos fricción. Y para eso, pensar mejor es tan importante como escribir más rápido.

Cuándo utilizar el Efecto Patito de Goma

Esta técnica puede aplicarse en muchos momentos del desarrollo, pero resulta especialmente útil cuando hay bloqueo mental o cuando el problema parece demasiado difuso.

Antes de pedir ayuda

Antes de escribir a un compañero o abrir una consulta en un canal del equipo, prueba a explicar el problema en voz alta. Es posible que encuentres la respuesta durante la explicación.

Y si no la encuentras, al menos habrás ordenado mejor la pregunta.

Antes de una revisión de código

También puedes usar esta técnica antes de abrir una pull request. Explicar qué has cambiado, por qué lo has cambiado y cómo funciona puede ayudarte a detectar detalles pendientes.

Por ejemplo:

  • nombres de variables poco claros;
  • funciones duplicadas;
  • validaciones incompletas;
  • comentarios innecesarios;
  • lógica demasiado acoplada;
  • casos límite no contemplados.

Durante el aprendizaje de programación

El Efecto Patito de Goma también es muy útil para personas que están aprendiendo a programar. Explicar un concepto en voz alta ayuda a comprobar si realmente se ha entendido.

Si puedes explicar qué hace un bucle, una promesa, un estado, una función o una condición, probablemente estás más cerca de dominarlo.

Para principiantes

En niveles iniciales, esta técnica ayuda a ganar confianza. No se trata de saberlo todo, sino de aprender a hacerse mejores preguntas.

Para perfiles con experiencia

En perfiles más avanzados, sirve para revisar arquitectura, patrones, flujos complejos o decisiones de diseño técnico. Incluso cuando hay experiencia, verbalizar sigue siendo una forma poderosa de detectar problemas.

Limitaciones del Efecto Patito de Goma

Aunque el Efecto Patito de Goma es muy útil, no es una solución mágica. Hay problemas que requieren herramientas específicas, pruebas automatizadas, revisión por pares o análisis más profundo.

No sustituye a los tests

Las pruebas unitarias, de integración o end-to-end siguen siendo fundamentales. El patito puede ayudarte a encontrar una hipótesis, pero los tests ayudan a comprobarla y evitar regresiones.

En otras palabras: explicar el problema puede ayudarte a descubrir el error, pero una buena estrategia de testing puede ayudarte a evitar que vuelva a aparecer.

No reemplaza la revisión de código

Explicar el código en voz alta puede ayudarte a mejorar una solución, pero la mirada de otra persona sigue aportando mucho valor. Un compañero puede detectar problemas de arquitectura, seguridad, rendimiento o mantenibilidad que quizá tú no ves.

El Efecto Patito de Goma no compite con el trabajo en equipo. Lo mejora, porque permite llegar a las conversaciones técnicas con una explicación más clara.

No resuelve falta de conocimiento técnico

A veces el bloqueo no se debe a un despiste, sino a que falta información. En esos casos, además de explicar el problema, será necesario consultar documentación, revisar ejemplos o pedir ayuda especializada.

El verdadero valor está en saber cuándo usar cada recurso.

Cómo combinar el Efecto Patito de Goma con otras buenas prácticas

El Rubber Duck Debugging funciona mejor cuando se integra dentro de una forma de trabajo ordenada.

Con documentación

Si al explicar un problema descubres que hay una parte del sistema difícil de entender, quizás conviene documentarla mejor. La documentación no solo ayuda a otras personas: también ayuda a tu “yo del futuro”.

Con comentarios útiles

No todo necesita comentarios, pero cuando una decisión técnica no es evidente, explicarla puede ser útil. Si al hablar con el patito descubres que una parte del código necesita demasiado contexto, quizás sea momento de mejorar nombres, dividir funciones o añadir una explicación breve.

Con testing

Después de detectar el error, conviene convertir ese aprendizaje en una prueba. Así evitas que el mismo problema vuelva a aparecer más adelante.

Con diseño de interacción

El Efecto Patito de Goma también puede ser útil cuando el problema no está solo en el código, sino en cómo se comporta una interfaz. Por ejemplo, cuando una animación no comunica bien un cambio de estado, cuando una transición parece confusa o cuando un elemento visual responde de forma inesperada.

En esos casos, explicar qué debería percibir la persona usuaria también ayuda a detectar incoherencias. Si estás trabajando este tipo de detalles, puede complementar bien esta guía sobre animaciones CSS.

Con pair programming

El Efecto Patito de Goma también puede convivir con el trabajo en pareja. De hecho, muchas conversaciones de pair programming funcionan de manera parecida: una persona explica, la otra escucha, pregunta y ayuda a ordenar el razonamiento.

La diferencia es que el patito está disponible siempre.

Ejemplo práctico de Efecto Patito de Goma en programación

Imagina que estás desarrollando una aplicación en React y tienes un componente que muestra una lista de tareas. El problema es que, al añadir una nueva tarea, la interfaz no se actualiza.

Podrías explicarlo así:

“Este componente recibe una lista de tareas desde el estado. Cuando el usuario escribe una nueva tarea y pulsa el botón, se ejecuta la función addTask. Esa función debería crear una nueva tarea y actualizar el estado con la lista anterior más la nueva.”

Mientras lo explicas, revisas el código y ves algo como esto:

tasks.push(newTask)

Después llamas a:

setTasks(tasks)

Al verbalizarlo, te das cuenta de que estás modificando directamente el array original en lugar de crear uno nuevo. En React, ese detalle puede impedir que el cambio se detecte correctamente.

La solución sería crear un nuevo array:

setTasks([...tasks, newTask])

Este ejemplo muestra muy bien el valor del Efecto Patito de Goma: el problema no era enorme, pero estaba oculto detrás de una suposición. Al explicar el flujo, el error se volvió visible.

Errores habituales al usar esta técnica

Aunque el Rubber Duck Debugging es sencillo, conviene evitar algunos errores para que realmente sea útil.

Explicar el problema de forma demasiado general

Si solo dices “esto no funciona”, no estás dando suficiente información. El objetivo es concretar el comportamiento esperado, el comportamiento real y el punto donde se produce la diferencia.

Saltar directamente a la solución

A veces queremos resolver tan rápido que no explicamos el problema completo. Pero la técnica funciona precisamente porque obliga a recorrer el proceso paso a paso.

No comprobar las hipótesis

Explicar ayuda, pero después hay que verificar. Si sospechas que una función no se ejecuta, compruébalo. Si crees que una variable llega vacía, revísala. Si piensas que una condición falla, analiza sus valores reales.

El Efecto Patito de Goma no sustituye la comprobación técnica. La orienta.

Preguntas frecuentes sobre el Efecto Patito de Goma

¿El Efecto Patito de Goma sirve solo para programadores?

No. Aunque nació dentro del contexto de la programación y el debugging, puede aplicarse a cualquier actividad que requiera resolver problemas complejos. Sirve para escribir, diseñar, planificar, estudiar o tomar decisiones. Sin embargo, en programación es especialmente útil porque el código exige lógica, secuencia y precisión.

¿Tengo que usar literalmente un patito de goma?

No necesariamente. El patito es una metáfora. Puedes usar cualquier objeto, una libreta, una nota de voz o incluso explicarte el problema a ti mismo en voz alta. Lo importante no es el objeto, sino el acto de convertir el pensamiento en una explicación clara y ordenada.

¿Cuándo debería pedir ayuda a otra persona?

Deberías pedir ayuda cuando ya has intentado definir el problema, revisar el flujo, comprobar tus hipótesis y aun así sigues sin avanzar. El Efecto Patito de Goma no pretende aislarte del equipo, sino ayudarte a llegar mejor preparado a la conversación. Pedir ayuda sigue siendo una parte esencial del desarrollo profesional.

Cuando explicar el problema se convierte en parte de la solución

El Efecto Patito de Goma nos recuerda algo importante: programar no consiste únicamente en escribir código. También consiste en pensar, ordenar ideas, formular hipótesis y revisar nuestras propias suposiciones.

A veces buscamos herramientas más sofisticadas, extensiones más completas o soluciones más complejas, cuando el primer paso puede ser tan sencillo como explicar el problema con calma. Porque al verbalizar lo que ocurre, dejamos de enfrentarnos a una confusión abstracta y empezamos a ver una secuencia concreta de decisiones.

El patito de goma no resuelve el bug. No revisa la arquitectura. No ejecuta tests. No lee documentación. Pero nos obliga a hacer algo que, en medio de la prisa, olvidamos con frecuencia: detenernos, observar y explicar.

Y muchas veces, cuando somos capaces de explicar bien un problema, ya estamos mucho más cerca de resolverlo.

Por eso el Rubber Duck Debugging sigue siendo una de las técnicas más simples y, al mismo tiempo, más poderosas dentro del desarrollo de aplicaciones. Porque mejora la lógica, favorece buenas prácticas de programación y ayuda a convertir el caos mental en claridad.

En definitiva, el Efecto Patito de Goma no es solo una técnica para depurar código. Es una forma de entrenar el pensamiento técnico. Y en programación, aprender a pensar con claridad puede marcar tanta diferencia como aprender una nueva herramienta.