Saltar al contenido
Explorar conocimiento

práctica · intermedio · 6 min de lectura

TDD

TDD guía el desarrollo escribiendo primero una prueba fallida, luego el mínimo código que la hace pasar y finalmente refactorizando con feedback inmediato.

No requiere conocimientos previos.

Alcance en breve

Cubre

  • test-driven development as a red-green-refactor practice
  • tests as executable design feedback before production code
  • practical boundaries between TDD, unit testing, and after-the-fact test coverage

No cubre

  • full testing strategy and test pyramid design
  • end-to-end automation frameworks
  • property-based testing and mutation testing

Supone

  • The reader understands functions, assertions, and the purpose of automated tests.

Resumen

TDD, o test-driven development, es una práctica de desarrollo donde el comportamiento nuevo se expresa primero como una prueba automatizada que falla. Después se escribe el mínimo código de producción para hacerla pasar y, con esa red de seguridad, se refactoriza.

Importa porque cambia el rol de los tests. No son solo verificación posterior; son una herramienta de diseño. Obligan a nombrar el comportamiento esperado antes de implementar, reducen ambigüedad y entregan feedback rápido sobre si el código sigue cumpliendo su contrato.

Alcance y supuestos

Este paquete cubre TDD como ciclo red-green-refactor: escribir una prueba fallida, hacerla pasar con implementación mínima y mejorar el diseño sin cambiar comportamiento. Usa TypeScript y Vitest como ejemplo por cercanía al stack del proyecto.

No cubre estrategia completa de testing, pirámide de tests, mocks avanzados, pruebas end-to-end, property-based testing ni mutation testing. Tampoco afirma que todo código deba nacer con TDD: hay exploración, spikes y UI visual donde primero puede convenir descubrir el diseño.

Modelo mental

Piensa en construir una pieza con una plantilla de medición. Antes de tallar la pieza, defines una guía que dice qué forma debe encajar. Al principio la pieza no entra: esa falla confirma que la guía detecta el comportamiento ausente. Luego cortas lo mínimo hasta que encaje. Recién después lijas y mejoras el acabado sin cambiar la forma que la plantilla valida.

La prueba fallida es la plantilla. El código mínimo es el primer corte. El refactor es el lijado. Si empiezas tallando sin plantilla, puedes terminar con algo que parece correcto, pero no tienes una señal clara de cuándo cumple exactamente lo pedido.

El valor de TDD no está en escribir más tests. Está en mantener un ciclo corto de intención, feedback y diseño. La prueba describe una pequeña decisión observable; el código responde a esa decisión; el refactor elimina ruido manteniendo verde la prueba.

Uso práctico

Usa TDD cuando el comportamiento puede expresarse como una expectativa observable y quieres que el diseño emerja con feedback rápido:

  • ✅ Reglas de negocio, validadores, parsers, cálculos y políticas suelen calzar bien con TDD.
  • ✅ Bugs reproducibles se benefician de una prueba roja que falla antes del fix.
  • ❌ Escribir tests después de terminar la implementación no es TDD; puede ser útil, pero no guió el diseño.
  • ❌ Forzar TDD durante una exploración visual o un spike incierto puede frenar aprendizaje; primero valida la idea, luego estabiliza con tests.

Ejemplo trabajado: aplicar un descuento con red-green-refactor

Supón que necesitas calcular el total de un carrito con descuento porcentual. En TDD, no empiezas escribiendo la función completa. Primero escribes una prueba que expresa un comportamiento pequeño y todavía falla porque la función no existe o no hace lo correcto.

import { describe, expect, it } from "vitest";
import { applyDiscount } from "./pricing";

describe("applyDiscount", () => {
  it("aplica un descuento porcentual al total", () => {
    expect(applyDiscount(100, 15)).toBe(85);
  });
});

Ese es el estado rojo: la prueba falla. La falla es útil porque demuestra que el test está conectado a un comportamiento que falta.

El siguiente paso no es diseñar todo el módulo de pricing. Es escribir el mínimo código que hace pasar ese caso:

export function applyDiscount(total: number, percent: number) {
  return total * (1 - percent / 100);
}

Ahora el estado es verde. La prueba pasó y tienes una implementación mínima para el comportamiento pedido. Recién ahí miras si el diseño necesita limpieza o si falta otro caso que exponga una regla nueva.

Por ejemplo, negocio aclara que el descuento no puede ser negativo ni mayor a 100. Agregas otra prueba antes de cambiar la implementación:

it("rechaza porcentajes fuera del rango 0 a 100", () => {
  expect(() => applyDiscount(100, -1)).toThrow("Invalid discount");
  expect(() => applyDiscount(100, 101)).toThrow("Invalid discount");
});

Vuelve el rojo. Luego haces verde el nuevo comportamiento:

export function applyDiscount(total: number, percent: number) {
  if (percent < 0 || percent > 100) {
    throw new Error("Invalid discount");
  }

  return total * (1 - percent / 100);
}

El ciclo evita mezclar muchas decisiones a la vez. Cada prueba nueva debe fallar por la razón esperada. Cada implementación debe hacer pasar el caso actual sin adelantarse demasiado. Cada refactor debe mantener la suite verde.

Antes de usar TDD en una tarea, revisa cuatro preguntas:

  1. ¿Puedo describir el siguiente comportamiento como una expectativa observable?
  2. ¿La prueba fallará si el comportamiento todavía no existe?
  3. ¿Puedo hacerla pasar con un cambio pequeño?
  4. ¿Hay diseño que puedo mejorar después sin cambiar el comportamiento?

Si las respuestas son sí, TDD da un ritmo seguro y enfocado. Si todavía no sabes qué comportamiento quieres, haz primero un spike corto y transforma lo aprendido en pruebas cuando el objetivo sea claro.

Evidencia

Conexiones

Relacionados y alternativas

Ver grafo local

Fuentes citadas