Español

Domina el Desarrollo Guiado por Pruebas (TDD) en JavaScript. Esta guía completa cubre el ciclo Rojo-Verde-Refactorizar, implementación práctica con Jest y las mejores prácticas para el desarrollo moderno.

Desarrollo Guiado por Pruebas en JavaScript: Una Guía Completa para Desarrolladores Globales

Imagina este escenario: tienes la tarea de modificar una pieza crítica de código en un sistema grande y heredado. Sientes una sensación de pavor. ¿Tu cambio romperá algo más? ¿Cómo puedes estar seguro de que el sistema sigue funcionando como se espera? Este miedo al cambio es una dolencia común en el desarrollo de software, que a menudo conduce a un progreso lento y a aplicaciones frágiles. Pero, ¿y si hubiera una forma de construir software con confianza, creando una red de seguridad que atrape los errores antes de que lleguen a producción? Esta es la promesa del Desarrollo Guiado por Pruebas (TDD).

TDD no es simplemente una técnica de prueba; es un enfoque disciplinado para el diseño y desarrollo de software. Invierte el modelo tradicional de "escribir código, luego probar". Con TDD, escribes una prueba que falla antes de escribir el código de producción para que pase. Esta simple inversión tiene profundas implicaciones en la calidad, el diseño y la mantenibilidad del código. Esta guía proporcionará una visión completa y práctica de la implementación de TDD en JavaScript, diseñada para una audiencia global de desarrolladores profesionales.

¿Qué es el Desarrollo Guiado por Pruebas (TDD)?

En su esencia, el Desarrollo Guiado por Pruebas es un proceso de desarrollo que se basa en la repetición de un ciclo de desarrollo muy corto. En lugar de escribir funcionalidades y luego probarlas, TDD insiste en que la prueba se escriba primero. Esta prueba inevitablemente fallará porque la funcionalidad aún no existe. El trabajo del desarrollador es entonces escribir el código más simple posible para que esa prueba específica pase. Una vez que pasa, el código se limpia y se mejora. Este bucle fundamental se conoce como el ciclo "Rojo-Verde-Refactorizar".

El Ritmo de TDD: Rojo-Verde-Refactorizar

Este ciclo de tres pasos es el latido del corazón de TDD. Comprender y practicar este ritmo es fundamental para dominar la técnica.

Una vez que el ciclo se completa para una pequeña pieza de funcionalidad, comienzas de nuevo con una nueva prueba que falla para la siguiente pieza.

Las Tres Leyes de TDD

Robert C. Martin (a menudo conocido como "Tío Bob"), una figura clave en el movimiento de software Ágil, definió tres reglas simples que codifican la disciplina de TDD:

  1. No debes escribir ningún código de producción a menos que sea para hacer que una prueba unitaria que falla pase.
  2. No debes escribir más de una prueba unitaria de lo que es suficiente para que falle; y los fallos de compilación son fallos.
  3. No debes escribir más código de producción del que es suficiente para pasar la única prueba unitaria que falla.

Seguir estas leyes te obliga a entrar en el ciclo Rojo-Verde-Refactorizar y asegura que el 100% de tu código de producción se escriba para satisfacer un requisito específico y probado.

¿Por Qué Deberías Adoptar TDD? El Caso de Negocio Global

Aunque TDD ofrece inmensos beneficios a los desarrolladores individuales, su verdadero poder se realiza a nivel de equipo y de negocio, especialmente en entornos distribuidos globalmente.

Configurando tu Entorno TDD de JavaScript

Para comenzar con TDD en JavaScript, necesitas algunas herramientas. El ecosistema moderno de JavaScript ofrece excelentes opciones.

Componentes Centrales de una Pila de Pruebas

Por su simplicidad y naturaleza todo-en-uno, usaremos Jest para nuestros ejemplos. Es una excelente opción para equipos que buscan una experiencia de "cero configuración".

Configuración Paso a Paso con Jest

Vamos a configurar un nuevo proyecto para TDD.

1. Inicializa tu proyecto: Abre tu terminal y crea un nuevo directorio de proyecto.

mkdir js-tdd-project
cd js-tdd-project
npm init -y

2. Instala Jest: Añade Jest a tu proyecto como una dependencia de desarrollo.

npm install --save-dev jest

3. Configura el script de prueba: Abre tu archivo `package.json`. Busca la sección `"scripts"` y modifica el script `"test"`. También es muy recomendable añadir un script `"test:watch"`, que es invaluable para el flujo de trabajo de TDD.

"scripts": {
  "test": "jest",
  "test:watch": "jest --watchAll"
}

El indicador `--watchAll` le dice a Jest que vuelva a ejecutar las pruebas automáticamente cada vez que se guarda un archivo. Esto proporciona retroalimentación instantánea, lo cual es perfecto para el ciclo Rojo-Verde-Refactorizar.

¡Eso es todo! Tu entorno está listo. Jest encontrará automáticamente los archivos de prueba que se llamen `*.test.js`, `*.spec.js`, o que estén ubicados en un directorio `__tests__`.

TDD en la Práctica: Construyendo un Módulo `CurrencyConverter`

Apliquemos el ciclo TDD a un problema práctico y globalmente entendido: convertir dinero entre monedas. Construiremos un módulo `CurrencyConverter` paso a paso.

Iteración 1: Conversión Simple con Tasa Fija

🔴 ROJO: Escribe la primera prueba que falle

Nuestro primer requisito es convertir una cantidad específica de una moneda a otra usando una tasa fija. Crea un nuevo archivo llamado `CurrencyConverter.test.js`.

// CurrencyConverter.test.js
const CurrencyConverter = require('./CurrencyConverter');

describe('CurrencyConverter', () => {
  it('debería convertir una cantidad de USD a EUR correctamente', () => {
    // Preparar
    const amount = 10; // 10 USD
    const expected = 9.2; // Asumiendo una tasa fija de 1 USD = 0.92 EUR

    // Actuar
    const result = CurrencyConverter.convert(amount, 'USD', 'EUR');

    // Afirmar
    expect(result).toBe(expected);
  });
});

Ahora, ejecuta el observador de pruebas desde tu terminal:

npm run test:watch

La prueba fallará estrepitosamente. Jest reportará algo como `TypeError: Cannot read properties of undefined (reading 'convert')`. Este es nuestro estado ROJO. La prueba falla porque `CurrencyConverter` no existe.

🟢 VERDE: Escribe el código más simple para que pase

Ahora, hagamos que la prueba pase. Crea `CurrencyConverter.js`.

// CurrencyConverter.js
const rates = {
  USD: {
    EUR: 0.92
  }
};

const CurrencyConverter = {
  convert(amount, from, to) {
    return amount * rates[from][to];
  }
};

module.exports = CurrencyConverter;

Tan pronto como guardes este archivo, Jest volverá a ejecutar la prueba y se pondrá en VERDE. Hemos escrito la cantidad mínima de código para satisfacer el requisito de la prueba.

🔵 REFACTORIZAR: Mejora el código

El código es simple, pero ya podemos pensar en mejoras. El objeto anidado `rates` es un poco rígido. Por ahora, es lo suficientemente limpio. Lo más importante es que tenemos una funcionalidad operativa protegida por una prueba. Pasemos al siguiente requisito.

Iteración 2: Manejo de Monedas Desconocidas

🔴 ROJO: Escribe una prueba para una moneda inválida

¿Qué debería pasar si intentamos convertir a una moneda que no conocemos? Probablemente debería lanzar un error. Definamos este comportamiento en una nueva prueba en `CurrencyConverter.test.js`.

// En CurrencyConverter.test.js, dentro del bloque describe

it('debería lanzar un error para monedas desconocidas', () => {
  // Preparar
  const amount = 10;

  // Actuar y Afirmar
  // Envolvemos la llamada a la función en una función de flecha para que toThrow de Jest funcione.
  expect(() => {
    CurrencyConverter.convert(amount, 'USD', 'XYZ');
  }).toThrow('Unknown currency: XYZ');
});

Guarda el archivo. El ejecutor de pruebas muestra inmediatamente un nuevo fallo. Está en ROJO porque nuestro código no lanza un error; intenta acceder a `rates['USD']['XYZ']`, resultando en un `TypeError`. Nuestra nueva prueba ha identificado correctamente este defecto.

🟢 VERDE: Haz que la nueva prueba pase

Modifiquemos `CurrencyConverter.js` para añadir la validación.

// CurrencyConverter.js
const rates = {
  USD: {
    EUR: 0.92,
    GBP: 0.80
  },
  EUR: {
    USD: 1.08
  }
};

const CurrencyConverter = {
  convert(amount, from, to) {
    if (!rates[from] || !rates[from][to]) {
      // Determinar qué moneda es desconocida para un mejor mensaje de error
      const unknownCurrency = !rates[from] ? from : to;
      throw new Error(`Unknown currency: ${unknownCurrency}`);
    }
    return amount * rates[from][to];
  }
};

module.exports = CurrencyConverter;

Guarda el archivo. Ambas pruebas ahora pasan. Estamos de vuelta en VERDE.

🔵 REFACTORIZAR: Límpialo

Nuestra función `convert` está creciendo. La lógica de validación está mezclada con el cálculo. Podríamos extraer la validación a una función privada separada para mejorar la legibilidad, pero por ahora, todavía es manejable. La clave es que tenemos la libertad de hacer estos cambios porque nuestras pruebas nos dirán si rompemos algo.

Iteración 3: Obtención Asincrónica de Tasas

Hardcodear las tasas no es realista. Refactoricemos nuestro módulo para obtener las tasas de una API externa (simulada).

🔴 ROJO: Escribe una prueba asincrónica que simule una llamada a la API

Primero, necesitamos reestructurar nuestro conversor. Ahora necesitará ser una clase que podamos instanciar, quizás con un cliente de API. También necesitaremos simular la API `fetch`. Jest lo hace fácil.

Reescribamos nuestro archivo de prueba para acomodar esta nueva realidad asincrónica. Empezaremos probando de nuevo el camino feliz.

// CurrencyConverter.test.js
const CurrencyConverter = require('./CurrencyConverter');

// Simular la dependencia externa
global.fetch = jest.fn();

beforeEach(() => {
  // Limpiar el historial del mock antes de cada prueba
  fetch.mockClear();
});

describe('CurrencyConverter', () => {
  it('debería obtener las tasas y convertir correctamente', async () => {
    // Preparar
    // Simular la respuesta exitosa de la API
    fetch.mockResolvedValueOnce({
      json: () => Promise.resolve({ rates: { EUR: 0.92 } })
    });

    const converter = new CurrencyConverter('https://api.exchangerates.com');
    const amount = 10; // 10 USD

    // Actuar
    const result = await converter.convert(amount, 'USD', 'EUR');

    // Afirmar
    expect(result).toBe(9.2);
    expect(fetch).toHaveBeenCalledTimes(1);
    expect(fetch).toHaveBeenCalledWith('https://api.exchangerates.com/latest?base=USD');
  });

  // También añadiríamos pruebas para fallos de la API, etc.
});

Ejecutar esto resultará en un mar de ROJO. Nuestro antiguo `CurrencyConverter` no es una clase, no tiene un método `async`, y no usa `fetch`.

🟢 VERDE: Implementa la lógica asincrónica

Ahora, reescribamos `CurrencyConverter.js` para cumplir con los requisitos de la prueba.

// CurrencyConverter.js
class CurrencyConverter {
  constructor(apiUrl) {
    this.apiUrl = apiUrl;
  }

  async convert(amount, from, to) {
    const response = await fetch(`${this.apiUrl}/latest?base=${from}`);
    if (!response.ok) {
      throw new Error('Failed to fetch exchange rates.');
    }

    const data = await response.json();
    const rate = data.rates[to];

    if (!rate) {
      throw new Error(`Unknown currency: ${to}`);
    }

    // Redondeo simple para evitar problemas de punto flotante en las pruebas
    const convertedAmount = amount * rate;
    return parseFloat(convertedAmount.toFixed(2));
  }
}

module.exports = CurrencyConverter;

Cuando guardes, la prueba debería volverse VERDE. Ten en cuenta que también agregamos lógica de redondeo para manejar imprecisiones de punto flotante, un problema común en los cálculos financieros.

🔵 REFACTORIZAR: Mejora el código asincrónico

El método `convert` está haciendo mucho: obtener datos, manejar errores, analizar y calcular. Podríamos refactorizar esto creando una clase `RateFetcher` separada, responsable únicamente de la comunicación con la API. Nuestro `CurrencyConverter` usaría entonces este fetcher. Esto sigue el Principio de Responsabilidad Única y hace que ambas clases sean más fáciles de probar y mantener. TDD nos guía hacia este diseño más limpio.

Patrones y Antipatrones Comunes de TDD

A medida que practiques TDD, descubrirás patrones que funcionan bien y antipatrones que causan fricción.

Buenos Patrones a Seguir

Antipatrones a Evitar

TDD en el Ciclo de Vida de Desarrollo Más Amplio

TDD no existe en el vacío. Se integra maravillosamente con las prácticas modernas de Agile y DevOps, especialmente para equipos globales.

Conclusión: Tu Viaje con TDD

El Desarrollo Guiado por Pruebas es más que una estrategia de prueba, es un cambio de paradigma en cómo abordamos el desarrollo de software. Fomenta una cultura de calidad, confianza y colaboración. El ciclo Rojo-Verde-Refactorizar proporciona un ritmo constante que te guía hacia un código limpio, robusto y mantenible. El conjunto de pruebas resultante se convierte en una red de seguridad que protege a tu equipo de regresiones y en una documentación viva que facilita la incorporación de nuevos miembros.

La curva de aprendizaje puede parecer empinada, y el ritmo inicial puede parecer más lento. Pero los dividendos a largo plazo en la reducción del tiempo de depuración, la mejora del diseño de software y el aumento de la confianza del desarrollador son inconmensurables. El viaje para dominar TDD es de disciplina y práctica.

Empieza hoy. Elige una característica pequeña y no crítica en tu próximo proyecto y comprométete con el proceso. Escribe la prueba primero. Mira cómo falla. Haz que pase. Y luego, lo más importante, refactoriza. Experimenta la confianza que proviene de un conjunto de pruebas en verde, y pronto te preguntarás cómo pudiste construir software de otra manera.

Desarrollo Guiado por Pruebas en JavaScript: Una Guía Completa para Desarrolladores Globales | MLOG