Capítulo 04 de 10 · 10 min de lectura

Qué es TDD: rojo, verde, refactor

El test se escribe antes que el código, y se ve fallar. Parece un rodeo. Es la forma de saber que el test comprueba algo.

Quién hace cumplir el contrato

Los dos capítulos anteriores dejan una especificación: criterios numerados, cada uno con su resultado exacto. Falta algo que la haga cumplir. Un contrato que nadie comprueba se queda en una declaración de intenciones, y un agente puede saltársela sin que nadie lo note.

Un test es un criterio que se comprueba solo. ALI-02 dice que un alias de dos caracteres da un error con un mensaje concreto; su test llama a la función con ab y compara lo que devuelve con ese mensaje. Se ejecuta en milisegundos, todas las veces que haga falta.

TDD, Test-Driven Development o desarrollo guiado por tests, es una regla sobre el orden: el test se escribe antes que el código que comprueba. El ciclo tiene tres pasos:

  1. Rojo. Se escribe el test y se ejecuta. Falla, porque el código todavía no existe.
  2. Verde. Se escribe lo mínimo para que el test pase. Nada más.
  3. Refactor. Con los tests en verde, se limpia el código sin cambiar lo que hace. Los tests avisan si algo se rompe.

Y se repite con el criterio siguiente.

El ciclo, paso a paso

La demo de la portada enseña las tres fotos del ciclo. Esta enseña el recorrido, criterio a criterio, con las reglas del alias de acorta. acorta está escrito en Go; aquí la función es JavaScript para que se ejecute en tu navegador, pero los tests se llaman igual que los de acorta y piden lo mismo.

Paso 1 de 8 · Verde sin código

El primer test, y pasa solo

ALI-01 dice que «oferta-otono» vale. El test está escrito, la función está vacía, y pasa. Un test que pasa sin código todavía no prueba nada: ganará sentido cuando haya reglas que puedan rechazar ese alias.

Los tests escritos hasta ahoraVerde · 1 de 1
  1. ALI-01 alias válidonuevo: pasa

    errorDeAlias("oferta-otono") → null (vale)

    obtenido: null

Activa JavaScript para recorrer los pasos y editar el código.

Recórrela entera con «Siguiente» y fíjate en cuatro cosas:

  • En el paso 1 hay un test en verde y ni una línea de código. Ese verde no vale nada. De eso trata la sección siguiente.
  • En los pasos rojos el código no cambia. Solo aparecen tests. El fallo dice qué falta y con qué entrada, sin que nadie tenga que deducirlo.
  • En los pasos verdes el código hace solo lo que piden los tests. En el paso 3 la función acepta Oferta y -oferta, y está bien: todavía no hay ningún test que diga lo contrario.
  • En el último paso el código cambia entero y no pasa nada. Los once tests siguen en verde. Si quien reescribe es un agente, esa es la única forma barata de saber que no ha cambiado el comportamiento.

Por qué el rojo va primero

Un test que nunca has visto fallar puede ser un test que no falla nunca. El del paso 1 pasa con una función que devuelve siempre null. Pasaría igual si la función no mirase el alias.

En acorta ocurrió a mayor escala. El agente que escribió los tests del dominio los ejecutó antes de que hubiera implementación, con todas las funciones vacías. Esto es parte de su informe:

Hay 97 casos hoja (antes 76): 84 fallan y 13 pasan con el esqueleto vacío.

Los trece que pasan son del mismo tipo que el del paso 1: comprobaciones negativas que una función vacía ya cumple, como que oferta no es una palabra reservada. No son tests malos. El de -Abc de la demo también nace en verde y es el que salta cuando alguien desordena las reglas. Pero hay que saber cuáles son, y solo se sabe ejecutando los tests antes de que exista el código. Por eso el encargo lo pedía:

Algún caso puede pasar con la función vacía (por ejemplo, «esto no es una palabra reservada»): es normal, pero dilo en el informe.

Importa también por qué falla. Un test puede estar en rojo porque el código no compila, porque llama a una función que no existe o porque el propio test tiene una errata. Ninguno de esos rojos demuestra nada. El encargo de acorta lo decía así:

Ejecuta go test ./link/ y comprueba que los tests fallan por las aserciones, no por errores de compilación.

Nos pasó construyendo esta web. En la demo de la portada, la función sin implementar era al principio una línea: throw new Error('sin implementar'). Uno de los siete criterios dice que un título sin letras debe lanzar un error, y su test pasaba: la función lanzaba un error, aunque no fuera ese. Seis en rojo y uno en verde por accidente. Lo cazó la comprobación de que, sin implementar, fallan los siete. Ahora la función de partida está vacía.

Con una IA, el orden importa más

Escribir el test después parece lo mismo. Cuando el código lo escribe una persona ya hay una diferencia; cuando lo escribe un modelo, hay dos.

La primera: un test escrito después se escribe mirando el código. Si le pides a un agente el código y sus tests en el mismo encargo, lo natural es que los tests comprueben lo que el código hace. Pasan a la primera, y eso tiene el mismo valor que el verde del paso 1. Si el código convierte Oferta en oferta, habrá un test que lo celebre, aunque la spec diga que es un error.

Un test escrito antes solo puede salir de la spec, porque no hay nada más que mirar.

La segunda: para llegar al verde hay dos caminos. Cuando un test falla, se puede arreglar el código o se puede cambiar el test. Los dos terminan en verde, y a un agente al que solo le has pedido «que pasen los tests» le valen los dos. Por eso acorta tiene esta regla en AGENTS.md:

No modifiques un test para que pase. Si crees que un test o un criterio están mal, párate y dilo.

Y por eso el rojo y el verde van en commits distintos. Lo que cambió entre el commit en rojo del dominio y el commit en verde, quitando la bitácora:

$ git diff --stat e6b8960 7343696 -- link/
 link/link.go | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 121 insertions(+), 6 deletions(-)

Un solo fichero, y no es el de los tests. No hace falta fiarse del informe del agente: se ve en el historial.

La regla tiene una segunda mitad, «párate y dilo», porque los tests también se equivocan. En acorta pasó con la herramienta de línea de comandos. El agente llegó al paso verde, vio que dos tests no los podía pasar ninguna implementación correcta, y en vez de retocarlos lo dijo. Uno medía el ancho de una columna en bytes, y la «Ó» de CÓDIGO ocupa dos. Se corrigieron en un commit aparte, a la vista:

69411fa cmd/acorta: corrige dos tests que ninguna implementación correcta podía pasar

El ciclo a escala de agente

La demo avanza de criterio en criterio. Es el TDD de manual, y es el ritmo razonable cuando escribes tú.

acorta no se hizo así, y conviene decirlo. Cada pieza se hizo en dos pasos grandes: primero todos los tests de su spec, después todo el código. El historial lo enseña:

e6b8960 link: tests de la spec 002 (rojo)
7343696 link: validación y generación de códigos (verde)
7cc3669 store: tests de la spec 003 (rojo)
a564bd2 store: almacén SQLite (verde)

El paso es más grande porque el coste es otro. El agente escribió los primeros 76 casos en 88 segundos; ir de uno en uno no le ahorraba nada. Lo que se conserva es lo que da valor al ciclo:

  • El orden. Los tests salen de la spec, antes de que haya código que mirar.
  • El rojo se ve y se guarda. El commit e6b8960 es la prueba: cualquiera puede descargarlo, ejecutar los tests y ver los 84 fallos.
  • En el verde no se tocan los tests. Y si alguno está mal, se dice.

Tampoco hay commits de refactor. La limpieza se pidió dentro del paso verde, con la condición del ciclo:

Cuando esté en verde, repasa el código y límpialo (nombres, duplicación, comentarios en español que expliquen el porqué) sin cambiar el comportamiento, con los tests en verde antes y después.

Este es el resultado, el código real de acorta para las reglas del alias. Compáralo con el último paso de la demo:

// checkAlias devuelve el primer error del alias (ya recortado y no vacío), o "".
func checkAlias(alias string) string {
	// Caracteres, no bytes: "ñu" son 2.
	if n := utf8.RuneCountInString(alias); n < minAlias || n > maxAlias {
		return "alias: debe tener entre 3 y 32 caracteres"
	}
	for _, r := range alias {
		if !(r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '-') {
			return "alias: solo admite minúsculas, números y guiones"
		}
	}
	if strings.HasPrefix(alias, "-") || strings.HasSuffix(alias, "-") {
		return "alias: no puede empezar ni terminar por guion"
	}
	if Reserved(alias) {
		return fmt.Sprintf("alias: %q está reservado", alias)
	}
	return ""
}

Las mismas cuatro reglas, en el mismo orden. No es casualidad: las dos funciones salen de los mismos criterios.

Lo que el verde no dice

Verde significa que el código hace lo que dicen los tests. No significa que los tests lo digan todo.

Si a la spec le falta un criterio, no hay test, y el verde no se entera. Si un criterio tiene test pero el test es flojo, tampoco. En el paso 3 de la demo todo estaba en verde con una función que aceptaba Oferta.

De eso tratan otros dos capítulos: el siguiente, de cómo se pasa de cada criterio a sus tests sin dejar huecos, y el último, de cómo se comprueba que los tests detectarían un fallo de verdad.

Lo que viene

Once tests para cinco criterios. ¿Por qué once, y por qué esos? El capítulo siguiente trata de cómo se convierte cada criterio en tests: los límites, los casos negativos, que son los primeros que se quedan fuera, y la forma de ver de un vistazo qué criterio se ha quedado sin el suyo.