El desarrollo guiado por pruebas (TDD, por sus siglas en inglés, Test-Driven Development) es una disciplina fundamental en el mundo del desarrollo de software moderno. No es solo una metodología para escribir pruebas, sino una filosofía de diseño que influye profundamente en la arquitectura de nuestro código, su mantenibilidad y su robustez. Para muchos, TDD puede parecer inicialmente contraintuitivo o una carga adicional, pero una vez que se experimentan sus beneficios, se convierte en una herramienta invaluable. Una de las mejores maneras de interiorizar TDD es a través de las "katas". Al igual que en las artes marciales, una kata de programación es un ejercicio repetitivo diseñado para mejorar una habilidad específica a través de la práctica constante y deliberada. En este post, exploraremos una kata TDD utilizando JavaScript, implementando un conocido problema que nos permitirá observar de primera mano el ciclo "rojo-verde-refactorizar". Mi objetivo es guiarte a través de un proceso que no solo te ayude a comprender TDD, sino que también te muestre cómo esta práctica puede llevar a un código más limpio y modular desde el principio.
¿Qué es una kata TDD?

Una kata TDD es un pequeño problema de programación que se resuelve aplicando rigurosamente los principios de TDD. La idea es practicar la técnica, no necesariamente resolver un problema complejo o crear una funcionalidad de negocio crítica. Es como los ejercicios de escalas para un músico o las formas para un artista marcial: son movimientos fundamentales que se repiten hasta que se convierten en algo instintivo. A través de las katas, los desarrolladores pueden mejorar su habilidad para escribir pruebas unitarias efectivas, refactorizar código sin miedo y, quizás lo más importante, diseñar software con una mentalidad orientada a la prueba, lo que a menudo resulta en un diseño más desacoplado y flexible.
Los beneficios de participar regularmente en katas TDD son múltiples:
* **Dominio de TDD:** Ayuda a automatizar el ciclo rojo-verde-refactorizar, haciendo que la escritura de pruebas sea una segunda naturaleza.
* **Mejora de la calidad del código:** Fomenta la creación de código más limpio, modular y fácil de entender.
* **Confianza en el refactoring:** Al tener una suite de pruebas robusta, los desarrolladores pueden refactorizar con la seguridad de que no están rompiendo funcionalidades existentes.
* **Claridad de requisitos:** La escritura de pruebas antes del código obliga a una comprensión más profunda de lo que la funcionalidad debe hacer.
* **Desarrollo de habilidades de diseño:** Promueve un diseño incremental y la identificación temprana de problemas de diseño.
A mi parecer, las katas son una herramienta subestimada en la formación continua de los desarrolladores. La teoría es importante, pero la práctica deliberada en un entorno de bajo riesgo es donde realmente se asientan las habilidades.
Configurando nuestro entorno de desarrollo en JavaScript
Para nuestra kata, utilizaremos JavaScript, un lenguaje extremadamente versátil y omnipresente. Como framework de pruebas, optaremos por Jest, una elección popular y robusta desarrollada por Facebook, conocida por su simplicidad y excelente rendimiento, especialmente en proyectos React, aunque es perfectamente adaptable a cualquier proyecto JS.
Preparando nuestro proyecto con Jest
Primero, asegúrate de tener
Node.js instalado en tu sistema. Una vez hecho esto, crearemos un nuevo directorio para nuestra kata y lo inicializaremos:
```bash
mkdir string-calculator-kata
cd string-calculator-kata
npm init -y
```
Ahora, instalaremos Jest como una dependencia de desarrollo:
```bash
npm install --save-dev jest
```
Para que Jest sea fácil de usar, añadiremos un script de prueba en nuestro `package.json`. Abre el archivo `package.json` y modifica la sección `scripts` para que se vea así:
```json
{
"name": "string-calculator-kata",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest --watchAll"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^29.7.0"
}
}
```
El flag `--watchAll` es muy útil porque Jest se ejecutará automáticamente cada vez que detecte cambios en nuestros archivos, facilitando el ciclo TDD. Ahora estamos listos para comenzar nuestra kata.
La kata: la calculadora de cadenas
La kata de la calculadora de cadenas (String Calculator) es un ejercicio clásico que nos permite explorar de forma incremental los pasos de TDD. Empezaremos con requisitos muy básicos y los iremos añadiendo poco a poco.
Nuestro objetivo es crear una función `add` que reciba una cadena de texto y devuelva un número.
Paso 1: cadena vacía
**Requisito:** Si la cadena de entrada está vacía, la función `add` debe devolver 0.
Este es nuestro punto de partida más simple.
**Código (Red): Escribimos la prueba que fallará.**
Crearemos dos archivos: `stringCalculator.js` para nuestra lógica y `stringCalculator.test.js` para nuestras pruebas.
`stringCalculator.js` (inicialmente vacío o con una exportación mínima):
```javascript
// stringCalculator.js
const add = (numbers) => {
// Nuestra implementación irá aquí
return 0; // Solo para que el primer test pase inicialmente si no queremos un error de "función no definida"
};
module.exports = add;
```
`stringCalculator.test.js`:
```javascript
// stringCalculator.test.js
const add = require('./stringCalculator');
describe('add', () => {
test('debería devolver 0 para una cadena vacía', () => {
expect(add('')).toBe(0);
});
});
```
Ejecuta `npm test`. Verás que la prueba falla (o pasa si ya hemos añadido `return 0;` en `add`). Si aún no has escrito la lógica, Jest te dirá que `add` no es una función o que falla la expectativa.
**Código (Green): Escribimos el código mínimo para que la prueba pase.**
`stringCalculator.js`:
```javascript
// stringCalculator.js
const add = (numbers) => {
if (numbers === '') {
return 0;
}
// No necesitamos más por ahora
};
module.exports = add;
```
Ejecuta `npm test` de nuevo. ¡Debería pasar! 🎉
**Refactorizar:** En este punto, no hay mucho que refactorizar. Nuestro código es simple y claro.
Paso 2: un solo número
**Requisito:** Si la cadena contiene un solo número, la función `add` debe devolver ese número.
**Código (Red): Escribimos la prueba que fallará.**
`stringCalculator.test.js`:
```javascript
// stringCalculator.test.js
const add = require('./stringCalculator');
describe('add', () => {
test('debería devolver 0 para una cadena vacía', () => {
expect(add('')).toBe(0);
});
test('debería devolver el número para una cadena con un solo número', () => {
expect(add('1')).toBe(1);
expect(add('5')).toBe(5);
});
});
```
Ejecuta `npm test`. Nuestras nuevas pruebas fallarán porque nuestra función actual solo maneja la cadena vacía.
**Código (Green): Escribimos el código mínimo para que las pruebas pasen.**
`stringCalculator.js`:
```javascript
// stringCalculator.js
const add = (numbers) => {
if (numbers === '') {
return 0;
}
return parseInt(numbers); // Convierte la cadena a número
};
module.exports = add;
```
Ejecuta `npm test`. ¡Todas las pruebas deberían pasar!
**Refactorizar:** Todavía es un código muy simple. `parseInt` es una función estándar y adecuada aquí.
Paso 3: dos números separados por coma
**Requisito:** Si la cadena contiene dos números separados por una coma, la función `add` debe devolver su suma.
**Código (Red): Escribimos la prueba que fallará.**
`stringCalculator.test.js`:
```javascript
// stringCalculator.test.js
// ...
test('debería devolver la suma de dos números separados por coma', () => {
expect(add('1,2')).toBe(3);
expect(add('5,5')).toBe(10);
});
// ...
```
Ejecuta `npm test`. Esta prueba fallará porque `parseInt('1,2')` no devolverá 3.
**Código (Green): Escribimos el código mínimo para que las pruebas pasen.**
`stringCalculator.js`:
```javascript
// stringCalculator.js
const add = (numbers) => {
if (numbers === '') {
return 0;
}
if (numbers.includes(',')) {
const parts = numbers.split(',');
const num1 = parseInt(parts[0]);
const num2 = parseInt(parts[1]);
return num1 + num2;
}
return parseInt(numbers);
};
module.exports = add;
```
Ejecuta `npm test`. ¡Todas las pruebas deberían pasar!
**Refactorizar:** Podemos empezar a ver cierta duplicación o caminos que se podrían unificar. Por ejemplo, si tenemos `1,2`, también podemos pensarlo como una lista de números.
Paso 4: múltiples números
**Requisito:** La función `add` debe manejar un número desconocido de números.
**Código (Red): Escribimos la prueba que fallará.**
`stringCalculator.test.js`:
```javascript
// stringCalculator.test.js
// ...
test('debería devolver la suma de múltiples números', () => {
expect(add('1,2,3')).toBe(6);
expect(add('10,20,30,40')).toBe(100);
});
// ...
```
Ejecuta `npm test`. Estas pruebas fallarán porque nuestra lógica actual solo maneja dos números.
**Código (Green): Escribimos el código mínimo para que las pruebas pasen.**
Para manejar múltiples números, podemos dividir la cadena por la coma y luego sumar todos los elementos.
`stringCalculator.js`:
```javascript
// stringCalculator.js
const add = (numbers) => {
if (numbers === '') {
return 0;
}
const numbersArray = numbers.split(',').map(num => parseInt(num));
return numbersArray.reduce((sum, current) => sum + current, 0);
};
module.exports = add;
```
Ejecuta `npm test`. ¡Todas las pruebas deberían pasar! Y mira, nuestro código se ha simplificado considerablemente. ¡Esto es TDD en acción! Al añadir un nuevo requisito, pudimos refactorizar y mejorar la solución existente. La belleza de esto es que cada refactorización se realiza con la seguridad de que nuestras pruebas previas nos alertarán si rompemos algo.
**Refactorizar:** Este código ya es bastante limpio. La cadena vacía se maneja elegantemente porque `split(',')` en una cadena vacía devuelve `['']`, y `parseInt('')` devuelve `NaN`. Sin embargo, `map(num => parseInt(num))` en `['']` resultaría en `[NaN]`, y `reduce` sobre `[NaN]` daría `NaN`. Esto significa que nuestra condición `if (numbers === '')` es aún necesaria. O podríamos filtrar los `NaN`s, pero por ahora, la condición explícita está bien y es legible.
Paso 5: manejar nuevas líneas como delimitadores
**Requisito:** Además de las comas, la función `add` debe aceptar nuevas líneas (`\n`) como delimitadores. Por ejemplo, `"1\n2,3"` debería devolver 6.
**Código (Red): Escribimos la prueba que fallará.**
`stringCalculator.test.js`:
```javascript
// stringCalculator.test.js
// ...
test('debería manejar nuevas líneas como delimitadores', () => {
expect(add('1\n2,3')).toBe(6);
expect(add('1\n2\n3')).toBe(6);
});
// ...
```
Ejecuta `npm test`. Fallarán porque `split(',')` no maneja `\n`.
**Código (Green): Escribimos el código mínimo para que las pruebas pasen.**
Ahora necesitamos dividir por comas O por nuevas líneas. Una forma es reemplazar las nuevas líneas con comas antes de dividir, o usar una expresión regular con `split`. Optemos por la expresión regular para mayor flexibilidad.
`stringCalculator.js`:
```javascript
// stringCalculator.js
const add = (numbers) => {
if (numbers === '') {
return 0;
}
// Dividir por coma o nueva línea
const numbersArray = numbers.split(/[\n,]/).map(num => parseInt(num));
return numbersArray.reduce((sum, current) => sum + current, 0);
};
module.exports = add;
```
Ejecuta `npm test`. ¡Todas las pruebas deberían pasar! ¡Fantástico!
**Refactorizar:** El código es ahora más robusto y conciso gracias a la expresión regular. Podríamos considerar un manejo más específico de `NaN` si queremos ser estrictos sobre entradas no numéricas, pero para esta kata, `parseInt` es suficiente.
Paso 6: soporte para delimitadores diferentes
**Requisito:** Se puede especificar un delimitador diferente al principio de la cadena. El formato será `"//[delimitador]\n[números]"`. Por ejemplo, `"//;\n1;2"` debería devolver 3.
**Código (Red): Escribimos la prueba que fallará.**
`stringCalculator.test.js`:
```javascript
// stringCalculator.test.js
// ...
test('debería soportar un delimitador personalizado', () => {
expect(add('//;\n1;2')).toBe(3);
expect(add('//|\n1|2|3')).toBe(6);
});
// ...
```
Ejecuta `npm test`. Estas pruebas fallarán.
**Código (Green): Escribimos el código mínimo para que las pruebas pasen.**
Necesitamos detectar si hay un delimitador personalizado. Si lo hay, lo extraemos y lo usamos para dividir la cadena.
`stringCalculator.js`:
```javascript
// stringCalculator.js
const add = (numbers) => {
if (numbers === '') {
return 0;
}
let delimiter = /[\n,]/; // Delimitador por defecto
let numbersToParse = numbers;
// Comprobar si hay un delimitador personalizado
if (numbers.startsWith('//')) {
const delimiterEndIndex = numbers.indexOf('\n');
const customDelimiter = numbers.substring(2, delimiterEndIndex);
delimiter = new RegExp(`[\\n,${customDelimiter}]`); // Incluir el nuevo delimitador
numbersToParse = numbers.substring(delimiterEndIndex + 1);
}
const numbersArray = numbersToParse.split(delimiter).map(num => parseInt(num));
return numbersArray.reduce((sum, current) => sum + current, 0);
};
module.exports = add;
```
Ejecuta `npm test`. ¡Todas las pruebas deberían pasar! Un pequeño detalle a tener en cuenta es que `new RegExp` necesita que los caracteres especiales se escapen si el delimitador personalizado es uno de ellos. Para un ejercicio más avanzado, podríamos manejar eso, pero para esta kata, asumiremos delimitadores simples. Para más detalles sobre expresiones regulares, puedes consultar la
documentación de MDN.
**Refactorizar:** Nuestro `add` se está volviendo un poco más complejo. La lógica para extraer el delimitador podría extraerse a una función auxiliar si crece mucho.
Paso 7: números negativos
**Requisito:** Si la cadena contiene números negativos, la función `add` debe lanzar una excepción con el mensaje "negativos no permitidos: " seguido de los números negativos encontrados.
**Código (Red): Escribimos la prueba que fallará.**
`stringCalculator.test.js`:
```javascript
// stringCalculator.test.js
// ...
test('debería lanzar una excepción para números negativos', () => {
expect(() => add('-1,2')).toThrow('negativos no permitidos: -1');
expect(() => add('1,-2,3,-4')).toThrow('negativos no permitidos: -2,-4');
expect(() => add('//;\n-1;-2')).toThrow('negativos no permitidos: -1,-2');
});
// ...
```
Ejecuta `npm test`. Estas pruebas fallarán.
**Código (Green): Escribimos el código mínimo para que las pruebas pasen.**
Después de parsear los números, debemos filtrarlos para encontrar los negativos.
`stringCalculator.js`:
```javascript
// stringCalculator.js
const add = (numbers) => {
if (numbers === '') {
return 0;
}
let delimiter = /[\n,]/;
let numbersToParse = numbers;
if (numbers.startsWith('//')) {
const delimiterEndIndex = numbers.indexOf('\n');
const customDelimiter = numbers.substring(2, delimiterEndIndex);
// Escapar el delimitador personalizado para la RegExp si fuera necesario
const escapedDelimiter = customDelimiter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
delimiter = new RegExp(`[\\n,${escapedDelimiter}]`);
numbersToParse = numbers.substring(delimiterEndIndex + 1);
}
const numbersArray = numbersToParse.split(delimiter).map(num => parseInt(num));
const negativeNumbers = numbersArray.filter(num => num 0);
if (negativeNumbers.length > 0) {
throw new Error(`negativos no permitidos: ${negativeNumbers.join(',')}`);
}
return numbersArray.reduce((sum, current) => sum + current, 0);
};
module.exports = add;
```
Ejecuta `npm test`. ¡Todas las pruebas deberían pasar! Nota que he añadido una pequeña mejora al escapar el delimitador personalizado en la expresión regular. Esto es un ejemplo de refactoring que se hace a menudo: al encontrar un caso que podría causar un error (un delimitador como `.` o `*`), lo corregimos inmediatamente.
Paso 8: ignorar números mayores de 1000
**Requisito:** Los números mayores de 1000 deben ser ignorados. Por ejemplo, `"1001,2"` debería devolver 2.
**Código (Red): Escribimos la prueba que fallará.**
`stringCalculator.test.js`:
```javascript
// stringCalculator.test.js
// ...
test('debería ignorar números mayores de 1000', () => {
expect(add('1000,1')).toBe(1001); // 1000 se incluye
expect(add('1001,2')).toBe(2);
expect(add('1000,2000,3')).toBe(1003);
});
}); // Cierre del describe
```
Ejecuta `npm test`. Fallarán.
**Código (Green): Escribimos el código mínimo para que las pruebas pasen.**
Añadimos un filtro después de manejar los negativos.
`stringCalculator.js`:
```javascript
// stringCalculator.js
const add = (numbers) => {
if (numbers === '') {
return 0;
}
let delimiter = /[\n,]/;
let numbersToParse = numbers;
if (numbers.startsWith('//')) {
const delimiterEndIndex = numbers.indexOf('\n');
const customDelimiter = numbers.substring(2, delimiterEndIndex);
const escapedDelimiter = customDelimiter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
delimiter = new RegExp(`[\\n,${escapedDelimiter}]`);
numbersToParse = numbers.substring(delimiterEndIndex + 1);
}
// Filtrar valores no válidos (NaN) antes de todo
let numbersArray = numbersToParse
.split(delimiter)
.map(num => parseInt(num))
.filter(num => !isNaN(num)); // Asegurarnos de que no hay NaN para la suma
const negativeNumbers = numbersArray.filter(num => num 0);
if (negativeNumbers.length > 0) {
throw new Error(`negativos no permitidos: ${negativeNumbers.join(',')}`);
}
const numbersToSum = numbersArray.filter(num => num = 1000);
return numbersToSum.reduce((sum, current) => sum + current, 0);
};
module.exports = add;
```
Ejecuta `npm test`. ¡Todas las pruebas deberían pasar! 🎉
Reflexiones sobre la kata y TDD
Al llegar a este punto, hemos recorrido un camino significativo en el desarrollo de nuestra función `add` utilizando TDD. Cada pequeño paso, guiado por una prueba fallida, nos ha llevado a una solución más completa y robusta. Esto es precisamente el poder de TDD: te obliga a pensar en el diseño y en los casos borde antes de escribir el código de producción.
Uno de los principales desafíos al inicio de TDD es superar la tentación de escribir más código del necesario para pasar la prueba actual. Esto requiere disciplina, pero con la práctica, se convierte en una segunda naturaleza. La pequeña cantidad de código escrita en cada iteración minimiza las oportunidades de introducir errores y facilita la depuración. Además, al mantener el código mínimo, se promueve un diseño incremental y emergente. Esto es, el diseño de tu solución "emerge" de las necesidades específicas de cada prueba, en lugar de ser una gran arquitectura predefinida que podría ser excesiva o incorrecta. Un recurso excelente para profundizar en TDD es el libro "