Introducción
El Desarrollo Guiado por Pruebas es un proceso de desarrollo de software donde los tests se escriben antes del código de producción. Sigue un ciclo corto y repetitivo: escribe un test que falle, escribe el código mínimo para que pase, luego refactoriza manteniendo los tests verdes.
El Ciclo Red-Green-Refactor
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Red │ → │ Green │ → │Refactor │
│ Escribe │ │ Código │ │ Mejora │
│ Falla │ │ Mínimo │ │ Diseño │
│ Test │ │ Pasa │ │ │
└─────────┘ └─────────┘ └─────────┘
↑ │
└───────────────────────────┘
1. Red — Escribe un Test que Falle
Comienza con un test que describe el comportamiento que deseas. Ejecútalo y observa fallar.
# test_calculator.py
def test_add_two_numbers():
calc = Calculator()
result = calc.add(2, 3)
assert result == 5
$ pytest test_calculator.py
FAILED: Calculator not defined
¿Por qué red primero? Un test que pasa sin código no prueba nada. Verlo fallar confirma que el test realmente está verificando algo.
2. Green — Escribe Código Mínimo
Escribe el código más simple que haga pasar el test. No te preocupes por la elegancia aún.
# calculator.py
class Calculator:
def add(self, a, b):
return a + b # implementación más simple posible
$ pytest test_calculator.py
PASSED
¿Por qué mínimo? Quieres el camino más corto a green. La abstracción prematura oscurece si el test realmente verifica lo correcto.
3. Refactor — Mejora el Diseño
Ahora que el test pasa, limpia: renombra variables, extrae métodos, elimina duplicación. Ejecuta los tests después de cada cambio.
# Refactorizado: aún pasa, pero más limpio
class Calculator:
def add(self, augend, addend):
return augend + addend
¿Por qué refactorizar en green? Los tests actúan como red de seguridad. Si un refactor rompe algo, lo sabes inmediatamente.
Un Ejemplo Completo
Construyamos un ShoppingCart usando TDD.
Paso 1: Carrito Vacío
def test_empty_cart_total_is_zero():
cart = ShoppingCart()
assert cart.total() == 0
class ShoppingCart:
def total(self):
return 0
Paso 2: Agregar Items
def test_add_item_increases_total():
cart = ShoppingCart()
cart.add(Item("manzana", 1.50))
assert cart.total() == 1.50
class ShoppingCart:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
def total(self):
return sum(item.price for item in self.items)
Paso 3: Aplicar Descuento
def test_apply_discount():
cart = ShoppingCart()
cart.add(Item("laptop", 1000))
cart.apply_discount("SAVE10")
assert cart.total() == 900
class ShoppingCart:
...
def apply_discount(self, code):
self.discount = 0.10 # hardcodeado por ahora
def total(self):
subtotal = sum(item.price for item in self.items)
if hasattr(self, 'discount'):
return subtotal * (1 - self.discount)
return subtotal
Paso 4: Refactorizar — Extraer Lógica de Descuento
class Discount:
def __init__(self, code, percentage):
self.code = code
self.percentage = percentage
def apply(self, amount):
return amount * (1 - self.percentage)
class ShoppingCart:
def __init__(self):
self.items = []
self.discount = None
def add(self, item):
self.items.append(item)
def apply_discount(self, code):
discounts = {"SAVE10": 0.10, "SAVE20": 0.20}
self.discount = Discount(code, discounts.get(code, 0))
def total(self):
subtotal = sum(item.price for item in self.items)
if self.discount:
return self.discount.apply(subtotal)
return subtotal
Las Tres Leyes de TDD
- No puedes escribir código de producción hasta tener un test unitario fallido.
- No puedes escribir más de un test unitario que sea suficiente para fallar. (Los errores de compilación cuentan como fallos.)
- No puedes escribir más código de producción que el suficiente para pasar el test actualmente fallido.
Beneficios de TDD
| Beneficio | Cómo TDD Lo Entrega |
|---|---|
| Confianza | Cada característica está respaldada por un test que prueba que funciona |
| Presión de diseño | El código debe ser testeable, lo cual tiende hacia diseños desacoplados y modulares |
| Documentación | Los tests son ejemplos ejecutables de cómo usar el código |
| Seguridad de regresión | Los cambios son seguros porque los tests existentes detectan roturas |
| Tiempo de debugging | Los bugs se detectan inmediatamente, no días después |
Errores Comunes de TDD
- Probar implementación, no comportamiento — haz assertions sobre valores de retorno, no sobre estado interno. Consulta unit testing.
- Escribir demasiados tests antes de código — mantén el ciclo corto (minutos, no horas)
- Saltar el paso de refactor — el tercer paso es donde mejora el código limpio
- Probar getters/setters triviales — enfócate en lógica y decisiones
- No ejecutar tests frecuentemente — si escribes 50 líneas sin ejecutar tests, no estás haciendo TDD
TDD vs. Pruebas Unitarias
| Pruebas Unitarias Tradicionales | TDD | |
|---|---|---|
| Cuándo se escriben los tests | Después del código | Antes del código |
| Cobertura de tests | A menudo incompleta | Exhaustiva por diseño |
| Influencia en diseño | Mínima | Importante (testabilidad guía el diseño) |
| Esfuerzo de debugging | Mayor | Menor |
Cuándo TDD Funciona Mejor
Usa TDD para:
- Lógica de negocio con entradas y salidas claras
- Código algorítmico
- APIs y límites de servicio
- Código que esperas cambiar frecuentemente
Usa con precaución en:
- Componentes de UI (usa tests de componente/E2E en su lugar)
- Prototipado exploratorio
- Código legacy acoplado (refactoriza para testeabilidad primero)
Lo que funciona
- Mantén los tests rápidos — una suite lenta desanima ejecutarla
- Un concepto por test — un fallo de test debe apuntar a exactamente un problema
- Usa nombres descriptivos en tests — el nombre debe explicar el escenario y resultado esperado
- Evita interdependencia de tests — cada test debe crear su propio estado
- Refactoriza tests también — setup duplicado es un olor; usa fixtures y helpers
Temas Avanzados
Escenario: TDD para API de Pagos
Sistema: API de pagos, 15 endpoints
Objetivo: TDD estricto (red-green-refactor)
Ciclo TDD por feature:
1. RED: Escribir test que falla
- Test: transferir $100 entre cuentas
- Resultado: FAIL (funcion no existe)
2. GREEN: Escribir codigo minimo que pasa
- Implementar transferencia basica
- Resultado: PASS
3. REFACTOR: Mejorar codigo sin romper tests
- Extraer validacion a funcion
- Resultado: PASS (sin cambios de comportamiento)
```javascript
// RED: Test para transferencia (falla)
describe("POST /api/transfers", () => {
it("debe transferir entre cuentas del mismo usuario", async () => {
const res = await request(app)
.post("/api/transfers")
.set("Authorization", `Bearer ${userToken}`)
.send({
sourceAccountId: accountA.id,
destinationAccountId: accountB.id,
amount: 100,
currency: "USD"
});
expect(res.status).toBe(201);
expect(res.body.status).toBe("completed");
// Verificar saldo actualizado
const src = await getAccount(accountA.id);
const dst = await getAccount(accountB.id);
expect(src.balance).toBe(originalA - 100);
expect(dst.balance).toBe(originalB + 100);
});
it("debe rechazar transferencia a cuenta ajena", async () => {
const res = await request(app)
.post("/api/transfers")
.set("Authorization", `Bearer ${userToken}`)
.send({
sourceAccountId: accountA.id,
destinationAccountId: otherUserAccount.id,
amount: 100,
currency: "USD"
});
expect(res.status).toBe(403);
});
it("debe rechitar monto negativo", async () => {
const res = await request(app)
.post("/api/transfers")
.set("Authorization", `Bearer ${userToken}`)
.send({
sourceAccountId: accountA.id,
destinationAccountId: accountB.id,
amount: -50,
currency: "USD"
});
expect(res.status).toBe(400);
});
});
Lecciones:
- RED primero: el test define el comportamiento esperado
- GREEN minimo: no agregues logica extra
- REFACTOR seguro: los tests protegen el cambio
- Un test por comportamiento, no por metodo
- Test nombres describen el comportamiento: “debe rechazar…”
- 0 flaky: tests deterministicos, sin dependencias externas
### Cuando NO usar TDD?
No uses TDD para exploracion (spikes, POCs) o cuando no sabes que vas a construir. TDD requiere saber el comportamiento esperado. Para UI exploratoria, prototipos o migraciones de datos, escribe codigo primero y tests despues. Para bugs, escribe primero el test que reproduce el bug (RED), luego fix (GREEN). Preguntas frecuentes
¿Cómo empiezo con esto en un proyecto existente?
Empieza con una parte pequeña y aislada de tu codebase. Aplica los conceptos de esta guía a un módulo o servicio. Mide el impacto, luego expande a otras áreas.
¿Qué herramientas necesito?
Las herramientas mencionadas throughout esta guía se listan en cada sección. La mayoría son open-source y ampliamente adoptadas. Consulta los recursos relacionados para instrucciones de setup.
¿Cómo mido el éxito después de implementar esto?
Define métricas claras antes de empezar: benchmarks de rendimiento, tasas de error o indicadores de mantenibilidad. Compara antes y después. Itera basándote en datos, no en suposiciones.
Recursos Relacionados
Guía de Estrategia de Testing
Una guía práctica para construir una estrategia de testing en capas con unit, integration y end-to-end tests.
GuidePrincipios de Código Limpio: Escribir Software Mantenible
Una guía práctica de código limpio: nombres significativos, funciones cortas, DRY, fundamentos SOLID y hábitos que hacen las bases de código más fáciles de leer y mantener.
GuidePrincipios SOLID Explicados con Ejemplos
Aprende los cinco principios SOLID con ejemplos prácticos de código: Responsabilidad Única, Abierto/Cerrado, Sustitución de Liskov, Segregación de Interfaces e Inversión de Dependencias.
RecipeAPI Mocking para Testing
Construye tests confiables mockeando APIs externas con WireMock, MockServer y MSW para eliminar flakiness y testear casos edge.
DocPlantilla de User Story y Criterios de Aceptación
Plantilla de user story que conecta necesidades de usuarios con implementación mediante criterios de aceptación claros, definición de done y principios INVEST.
DocPlantilla de Estrategia de Testing de API
Una plantilla para planificar tests de contrato, integración y carga para APIs.