En el vertiginoso mundo del desarrollo de software, la promesa de entregar código funcional, mantenible y libre de errores es un ideal al que todos aspiramos. Sin embargo, la realidad a menudo nos golpea con proyectos complejos, plazos ajustados y la constante amenaza de los "bugs" que acechan en las sombras. ¿Cuántas veces hemos sentido ese escalofrío al modificar una pieza de código, sin saber qué otra cosa podría romperse? ¿O la frustración de depurar durante horas un problema que podría haberse evitado?
Existe una disciplina que, si se adopta y practica con diligencia, puede transformar radicalmente nuestra forma de abordar estos desafíos: el Desarrollo Guiado por Pruebas (TDD, por sus siglas en inglés). TDD no es solo una técnica de pruebas; es una metodología de diseño, una forma de pensar que prioriza la claridad, la robustez y la confianza en cada línea de código que escribimos. Y para dominar cualquier disciplina, la práctica es fundamental. Aquí es donde entran en juego las "Katas de Código".
Este post no solo te introducirá en el fascinante mundo de TDD, sino que te guiará, paso a paso, a través de una Kata de código específica, implementada con JavaScript y el popular framework de pruebas Jest. Nuestro objetivo es que, al finalizar, no solo entiendas los conceptos, sino que hayas "sentido" el flujo de TDD, experimentando cómo pequeñas victorias consecutivas construyen una solución sólida y bien diseñada. Prepárate para afilar tus habilidades y transformar tu enfoque de desarrollo.
¿Por Qué TDD? Más Allá de los Tests

Antes de sumergirnos en el código, es crucial entender la filosofía detrás de TDD. Muchos desarrolladores, al escuchar "Desarrollo Guiado por Pruebas", asumen que se trata únicamente de escribir muchos tests. Si bien la creación de pruebas automatizadas es un componente clave, la esencia de TDD radica en su proceso cíclico: Rojo, Verde, Refactor.
- Rojo (Red): Escribe una pequeña prueba que falle. Esta prueba debe describir un nuevo comportamiento que deseas añadir o una característica que quieres implementar. Es importante que la prueba falle inicialmente, demostrando que el comportamiento aún no existe. Este paso te obliga a pensar en la interfaz de tu código antes de la implementación, clarificando las expectativas.
- Verde (Green): Escribe el código mínimo indispensable para que la prueba que acaba de fallar pase. Y cuando digo mínimo, me refiero a realmente mínimo. No intentes resolver el problema completo; solo lo suficiente para satisfacer la prueba actual. Este enfoque fomenta la simplicidad y evita la sobreingeniería prematura.
- Refactor (Refactor): Una vez que todas las pruebas están en verde, es el momento de mejorar la calidad del código. Sin cambiar el comportamiento externo (garantizado por tus pruebas en verde), puedes reorganizar el código, eliminar duplicidades, mejorar la legibilidad, cambiar nombres de variables o extraer funciones. El conjunto de pruebas te sirve como una red de seguridad, asegurando que tus cambios internos no introduzcan nuevos errores.
Este ciclo iterativo y disciplinado ofrece una miríada de beneficios que van mucho más allá de la simple verificación de errores:
- Mejora el Diseño del Software: Al pensar primero en cómo se va a usar tu código (a través de la prueba), te obligas a diseñar interfaces limpias, módulos cohesionados y bajo acoplamiento. TDD te empuja a crear código más fácil de testear, lo que generalmente se traduce en código más modular y robusto.
- Reduce los Bugs: Al detectar errores en etapas muy tempranas del desarrollo, el costo de corregirlos es significativamente menor. Cada nueva característica se construye sobre una base probada y sólida.
- Documentación Viva: Tus tests actúan como una especificación ejecutable del comportamiento de tu sistema. Son la documentación más precisa y siempre actualizada que existe.
- Confianza en el Refactoring: ¿Temes refactorizar ese módulo crítico porque "podría romper algo"? Con una suite de tests robusta, puedes realizar cambios significativos con la certeza de que, si algo se rompe, tus pruebas te lo indicarán inmediatamente. Esta confianza es invaluable para mantener la salud del código a largo plazo.
- Impulso Psicológico: Cada vez que una prueba pasa (pasa de Rojo a Verde), obtienes una pequeña victoria, un impulso de motivación. Estos pequeños éxitos acumulativos mantienen la moral alta y el progreso constante.
Mi experiencia personal me ha demostrado que, si bien al principio puede parecer una ralentización, TDD, a medio y largo plazo, acelera el desarrollo y mejora drásticamente la calidad del producto final. No lo veo como una carga, sino como una herramienta poderosa para arquitectos y desarrolladores que buscan construir sistemas verdaderamente resilientes.
Para aquellos interesados en profundizar en los fundamentos, recomiendo encarecidamente la lectura de "Test-Driven Development by Example" de Kent Beck, el libro seminal sobre el tema. Es una base excelente para entender el "por qué" detrás del proceso. Puedes encontrar más información sobre sus principios en recursos como este artículo de Martin Fowler sobre TDD.
Martin Fowler: Test-Driven Development (TDD)
La Kata, El Gimnasio del Desarrollador
Si TDD es una disciplina, una Kata de Código es el dojo donde la practicamos. Inspiradas en las artes marciales, donde las "katas" son secuencias de movimientos predefinidos que se repiten una y otra vez para perfeccionar una técnica, las Katas de Código son pequeños problemas de programación que resolvemos repetidamente. No se trata de encontrar la solución más rápida o ingeniosa, sino de practicar una habilidad específica, en nuestro caso, el ciclo TDD.
Las Katas son ideales para:
- Construir Memoria Muscular: La repetición de los pasos Rojo, Verde, Refactor, Rojo, Verde, Refactor, ayuda a internalizar el proceso, haciéndolo más natural y automático.
- Explorar Nuevas Tecnologías: Puedes aplicar una Kata conocida a un nuevo lenguaje o framework para familiarizarte con su ecosistema de pruebas y patrones comunes.
- Dominar Principios de Diseño: Muchas Katas están diseñadas para forzarte a aplicar principios de SOLID o de buen diseño de software.
Para nuestra demostración, elegiremos una Kata clásica y excelente para TDD: la "Calculadora de Cadenas" (String Calculator). Este problema, popularizado por Roy Osherove, es perfecto porque comienza de manera trivial y añade complejidad de forma incremental, permitiendo que el ciclo TDD brille en cada paso.
Puedes encontrar una lista de Katas muy útiles en sitios como Code Kata de Dave Thomas:
Preparando el Terreno: Configuración de Entorno JS para TDD
Para nuestra Kata en JavaScript, necesitaremos un entorno de ejecución (Node.js) y un framework de pruebas. Jest es una opción excelente, muy popular en el ecosistema de JavaScript por su facilidad de uso, velocidad y su capacidad para realizar snapshots, mocking, etc.
Si aún no tienes Node.js instalado, descárgalo desde su sitio oficial. Una vez que lo tengas, abre tu terminal y sigue estos pasos:
-
Crea un Nuevo Proyecto:
mkdir string-calculator-kata cd string-calculator-kata npm init -y
npm init -y
inicializa un nuevo proyecto Node.js con valores predeterminados, creando un archivopackage.json
. -
Instala Jest:
npm install --save-dev jest
Esto instalará Jest como una dependencia de desarrollo y lo añadirá a tu
package.json
. -
Configura el Script de Prueba: Abre tu archivo
package.json
y busca la secciónscripts
. Modifica el scripttest
para que se vea así:{ "name": "string-calculator-kata", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "jest --watchAll", "test:coverage": "jest --coverage" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "jest": "^29.7.0" } }
El comando
jest --watchAll
es muy útil para TDD, ya que Jest se mantendrá en ejecución y volverá a ejecutar las pruebas automáticamente cada vez que detecte cambios en tus archivos. -
Crea los Archivos: Necesitaremos dos archivos principales:
-
stringCalculator.js
: Aquí es donde residirá nuestra lógica de la calculadora. -
stringCalculator.test.js
: Aquí escribiremos nuestras pruebas.
-
¡Listo! Con esto, tenemos nuestro "dojo" preparado para empezar a practicar TDD. Para aprender más sobre Jest y sus capacidades, la documentación oficial es un recurso inmejorable:
Jest Documentation: Getting Started
Manos a la Obra: Una Kata de Calculadora de Cadenas con TDD
Nuestro objetivo es implementar una función add(numbers)
que toma una cadena de números y devuelve su suma. Vamos a abordar esto en pequeños incrementos, siguiendo el ciclo Rojo-Verde-Refactor de TDD.
Paso 1: Cadena Vacía
Requisito: add("")
debería devolver 0
.
1. ROJO: Escribir la prueba fallida.
Crea stringCalculator.test.js
:
// stringCalculator.test.js
const add = require('./stringCalculator'); // Importaremos la función aquí
describe('stringCalculator', () => {
test('should return 0 for an empty string', () => {
expect(add('')).toBe(0);
});
});
Ahora, ejecuta las pruebas: npm test
.
¡Fallo esperado! Verás un error como add is not a function
o add is undefined
. Perfecto.
2. VERDE: Escribir el código mínimo para pasar la prueba.
Crea stringCalculator.js
:
// stringCalculator.js
function add(numbers) {
return 0; // La solución más simple para que la primera prueba pase
}
module.exports = add;
Vuelve a ejecutar npm test
(si estás usando --watchAll
, se ejecutará automáticamente). ¡Verde!
3. REFACTOR: Mejorar el código (si es necesario). En este punto, el código es tan simple que no hay mucho que refactorizar. Sin embargo, es vital recordar que este paso es inherente al ciclo. A medida que el código crece, esta etapa se vuelve crítica.
Paso 2: Un Solo Número
Requisito: add("1")
debería devolver 1
, add("5")
debería devolver 5
.
1. ROJO: Escribir la prueba fallida.
Añade una nueva prueba a stringCalculator.test.js
:
// stringCalculator.test.js (continuación)
// ...
describe('stringCalculator', () => {
// ... (prueba anterior)
test('should return the number itself when given a single number string', () => {
expect(add('1')).toBe(1);
expect(add('5')).toBe(5);
});
});
npm test
. ¡Fallo esperado! Nuestra implementación actual devolverá 0
para cualquier entrada.
2. VERDE: Escribir el código mínimo para pasar las pruebas.
Modifica stringCalculator.js
:
// stringCalculator.js (continuación)
function add(numbers) {
if (numbers === '') {
return 0;
}
return parseInt(numbers, 10); // Convertir el número de cadena a entero
}
module.exports = add;
npm test
. ¡Verde! Ambas pruebas pasan.
3. REFACTOR:
El código sigue siendo bastante simple. parseInt
es una buena elección. No hay refactorización obvia aquí.
Paso 3: Dos Números
Requisito: add("1,2")
debería devolver 3
.
1. ROJO:
Añade a stringCalculator.test.js
:
// stringCalculator.test.js (continuación)
// ...
describe('stringCalculator', () => {
// ... (pruebas anteriores)
test('should return the sum of two numbers separated by a comma', () => {
expect(add('1,2')).toBe(3);
});
});
npm test
. ¡Fallo! parseInt('1,2')
probablemente devolverá 1
o NaN
, no 3
.
2. VERDE:
Modifica stringCalculator.js
:
// stringCalculator.js (continuación)
function add(numbers) {
if (numbers === '') {
return 0;
}
const numsArray = numbers.split(','); // Divide la cadena por la coma
if (numsArray.length === 1) { // Si solo hay un número, lo parseamos
return parseInt(numbers, 10);
} else { // Si hay varios, los sumamos
return numsArray.reduce((sum, num) => sum + parseInt(num, 10), 0);
}
}
module.exports = add;
npm test
. ¡Verde!
3. REFACTOR:
Podemos simplificar un poco la lógica. split
siempre devolverá un array, incluso para una cadena vacía (['']). Podemos eliminar la verificación de numsArray.length === 1
si manejamos el caso de cadena vacía correctamente con filter
.
// stringCalculator.js (refactorizado)
function add(numbers) {
if (numbers === '') {
return 0;
}
// Dividimos la cadena por la coma. Si hay una cadena vacía después de split (ej: "1,"), la ignoramos.
const numsArray = numbers.split(',').map(numStr => parseInt(numStr, 10));
// Ahora, si el array está vacío o contiene NaN (si la cadena original era inválida),
// necesitamos asegurarnos de que el caso "" devuelva 0, lo cual ya está cubierto.
// Para los demás casos, podemos simplemente sumar.
return numsArray.reduce((sum, num) => sum + num, 0);
}
module.exports = add;
¡Verde! Esta versión es más concisa. El paso de refactorización nos permitió simplificar la estructura.
Paso 4: Múltiples Números
Requisito: add("1,2,3")
debería devolver 6
.
1. ROJO:
// stringCalculator.test.js (continuación)
// ...
describe('stringCalculator', () => {
// ... (pruebas anteriores)
test('should return the sum of multiple numbers separated by commas', () => {
expect(add('1,2,3')).toBe(6);
expect(add('10,20,30,40')).toBe(100);
});
});
npm test
. ¡Debería estar en verde! Nuestro refactor en el paso anterior ya cubrió este caso. Esto demuestra que un buen refactor puede adelantar soluciones a futuros requisitos. Si hubiera fallado, simplemente ajustaríamos la lógica.
2. VERDE: (Ya estamos en verde).
3. REFACTOR: (Ya lo hicimos).
Paso 5: Nuevas Líneas como Delimitadores
Requisito: Permitir nuevas líneas (\n
) entre números, además de comas. add("1\n2,3")
debería devolver 6
.
1. ROJO:
// stringCalculator.test.js (continuación)
// ...
describe('stringCalculator', () => {
// ... (pruebas anteriores)
test('should handle new lines as delimiters', () => {
expect(add('1\n2,3')).toBe(6);
expect(add('1\n2\n3')).toBe(6);
});
});
npm test
. ¡Fallo! Nuestra función split(',')
no manejará \n
.
2. VERDE:
Modifica stringCalculator.js
. Necesitamos dividir por comas o nuevas líneas. Una expresión regular es ideal aquí.
// stringCalculator.js (continuación)
function add(numbers) {
if (numbers === '') {
return 0;
}
// Dividimos por coma (,) o por nueva línea (\n)
const numsArray = numbers
.split(/,|\n/) // Aquí usamos una RegEx para múltiples delimitadores
.map(numStr => parseInt(numStr, 10))
.filter(num => !isNaN(num)); // Filtra cualquier NaN resultante de cadenas vacías entre delimitadores (e.g., "1,\n2")
return numsArray.reduce((sum, num) => sum + num, 0);
}
module.exports = add;
npm test
. ¡Verde!
3. REFACTOR:
El .filter(!isNaN(num))
es un buen añadido para manejar entradas como 1,,2
o 1\n\n2
correctamente, donde parseInt('')
resultaría en NaN
. El código es bastante limpio.
Paso 6: Delimitadores Personalizados
Requisito: La cadena puede comenzar con //[delimiter]\n[numbers]
. Por ejemplo, add("//;\n1;2")
debería devolver 3
.
1. ROJO:
// stringCalculator.test.js (continuación)
// ...
describe('stringCalculator', () => {
// ... (pruebas anteriores)
test('should handle custom delimiters', () => {
expect(add('//;\n1;2')).toBe(3);
expect(add('//|\n1|2|3')).toBe(6);
});
});
npm test
. ¡Fallo!
2. VERDE:
Esta es la parte más interesante. Necesitamos detectar si la cadena comienza con //
y extraer el delimitador y luego los números.
// stringCalculator.js (continuación)
function 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 parts = numbers.split('\n', 2); // Divide la cadena en hasta 2 partes por la primera nueva línea
const delimiterDef = parts[0]; // Ej: "//;"
delimiter = new RegExp(delimiterDef.substring(2)); // Extrae el delimitador (ej: ";") y crea una RegEx
numbersToParse = parts[1]; // Los números restantes (ej: "1;2")
}
const numsArray = numbersToParse
.split(del