Skip to content
StackPractices
beginner Por Mathias Paulenko

Desarrollo Guiado por Pruebas (TDD)

Aprende TDD paso a paso: escribe un test que falle, hazlo pasar, refactoriza. Red-Green-Refactor con ejemplos reales en Python, JavaScript y Java.

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

Desarrollo Guiado por Pruebas (TDD)

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

  1. No puedes escribir código de producción hasta tener un test unitario fallido.
  2. No puedes escribir más de un test unitario que sea suficiente para fallar. (Los errores de compilación cuentan como fallos.)
  3. No puedes escribir más código de producción que el suficiente para pasar el test actualmente fallido.

Beneficios de TDD

BeneficioCómo TDD Lo Entrega
ConfianzaCada característica está respaldada por un test que prueba que funciona
Presión de diseñoEl código debe ser testeable, lo cual tiende hacia diseños desacoplados y modulares
DocumentaciónLos tests son ejemplos ejecutables de cómo usar el código
Seguridad de regresiónLos cambios son seguros porque los tests existentes detectan roturas
Tiempo de debuggingLos 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 TradicionalesTDD
Cuándo se escriben los testsDespués del códigoAntes del código
Cobertura de testsA menudo incompletaExhaustiva por diseño
Influencia en diseñoMínimaImportante (testabilidad guía el diseño)
Esfuerzo de debuggingMayorMenor

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

Preguntas Frecuentes

P: ¿TDD ralentiza el desarrollo? R: Inicialmente sí, pero se recupera en debugging reducido y refactoring más seguro. Estudios muestran que TDD puede reducir tasas de defectos en 40-90%.

P: ¿Qué pasa si no sé cómo debería verse la API aún? R: TDD es una herramienta de diseño. Escribir el test primero te ayuda a descubrir la forma de la API. Si realmente estás explorando, un spike rápido está bien — luego reescribe con TDD una vez que entiendas el problema.

P: ¿Debería usar TDD para cada función? R: No. Enfócate en código con comportamiento que valga la pena verificar. Objetos simples de transferencia de datos o configuración a menudo no necesitan tests unitarios dedicados.

¿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.

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).