Guía práctica
El test de contraste para verificar código generado por IA
Un método con Go para comprobar que un test hace fallar una alternativa incorrecta pero plausible, en lugar de limitarse a repetir la implementación generada.
Un agente recibe esta tarea:
Añade un búfer con capacidad limitada.
Implementa Push, añade tests y deja todo verde. En review parece terminado.
Sin embargo, la tarea permitía al menos dos comportamientos incompatibles cuando el búfer se llena:
- rechazar el elemento nuevo y conservar los existentes;
- eliminar el más antiguo y aceptar el nuevo.
Los dos son razonables. Solo uno puede ser el contrato del módulo.
Si los tests pasan con ambos, no verifican esa decisión. Verifican algo menos importante, como que el primer elemento puede insertarse.
Este artículo propone una comprobación concreta:
Un test especifica una decisión solo si hace fallar una alternativa incorrecta pero plausible.
Llamaremos test de contraste al caso diseñado para separar esas dos alternativas.
El test verde que no decide nada#
Supongamos esta API en Go:
subject, _ := buffer.New(1)
err := subject.Push("first")El agente genera este test:
func TestPushStoresAnItem(t *testing.T) {
subject, err := buffer.New(1)
if err != nil {
t.Fatalf("new buffer: %v", err)
}
if err := subject.Push("first"); err != nil {
t.Fatalf("push: %v", err)
}
got := subject.Items()
if len(got) != 1 || got[0] != "first" {
t.Fatalf("expected first item, got %v", got)
}
}El test es correcto, pero no cubre la decisión arriesgada. Pasaría tanto con una implementación que rechaza al llenarse como con otra que sobrescribe el elemento anterior.
No falta “más coverage” en abstracto. Falta un caso que distinga dos semánticas.
Escribe primero las dos alternativas#
Antes de añadir otro test, describe el contraste sin código:
Capacidad: 1
Estado inicial: [first]
Acción: Push(second)
Contrato elegido:
devuelve Full
estado final: [first]
Alternativa que queremos rechazar:
devuelve nil
estado final: [second]Este pequeño cuadro hace dos cosas.
Primero, obliga a confirmar una decisión que la tarea original no contenía. Si no existe en el ticket, la documentación o un consumidor actual, no debemos dejar que el agente la elija en silencio.
Segundo, muestra exactamente qué observaciones necesita el test: el resultado de Push y el contenido posterior. No necesitamos inspeccionar índices, llamadas internas ni la estructura usada por el búfer.
El test de contraste#
El caso puede escribirse así:
func TestFullBufferRejectsNewItemAndKeepsExistingOne(t *testing.T) {
subject, err := buffer.New(1)
if err != nil {
t.Fatalf("new buffer: %v", err)
}
if err := subject.Push("first"); err != nil {
t.Fatalf("push first item: %v", err)
}
err = subject.Push("second")
if !errors.Is(err, buffer.ErrFull) {
t.Fatalf("expected full error, got %v", err)
}
got := subject.Items()
if len(got) != 1 || got[0] != "first" {
t.Fatalf("expected first item to remain, got %v", got)
}
}El nombre declara la diferencia. Las dos aserciones son necesarias:
- comprobar solo
ErrFullpermitiría una implementación que devuelve el error después de sobrescribir el estado; - comprobar solo
[first]permitiría una implementación que ignora silenciosamente el segundo elemento.
Juntas protegen la garantía: el rechazo es visible y no altera el contenido existente.
Comprueba el test con una mutación#
Después de obtener el verde, introduce durante una sola ejecución la alternativa que quieres rechazar.
La implementación correcta podría contener:
func (b *Buffer) Push(item string) error {
if len(b.items) == b.capacity {
return ErrFull
}
b.items = append(b.items, item)
return nil
}Sustitúyela temporalmente por el comportamiento contrario:
func (b *Buffer) Push(item string) error {
if len(b.items) == b.capacity {
b.items = append(b.items[1:], item)
return nil
}
b.items = append(b.items, item)
return nil
}Ejecuta solo el caso:
go test ./buffer -run TestFullBufferRejectsNewItemAndKeepsExistingOneEl resultado esperado es un fallo semántico:
expected full error, got <nil>Restaura la implementación y vuelve a ejecutar.
Esta mutación manual responde una pregunta que el porcentaje de cobertura no puede contestar: ¿el test detecta la decisión equivocada que nos preocupa?
No hace falta mutar cada línea. Basta con probar las alternativas que eran plausibles antes de conocer la implementación.
No derives la alternativa del diff#
El contraste debe proceder de la tarea, de los consumidores o de una decisión explícita. Si lo inventamos después de leer el código generado, corremos el riesgo de validar solamente la forma elegida por el agente.
Una secuencia útil es:
- antes del código, identifica una decisión con dos resultados incompatibles;
- confirma cuál pertenece al contrato;
- escribe el caso que separa ambos;
- después del verde, introduce por una ejecución la alternativa rechazada;
- conserva el test solo si falla por la diferencia esperada.
Por ejemplo, otros contrastes posibles serían:
- Clave ausente: devolver
NotFoundfrente a devolver un valor vacío. - Reintento de comando: conservar el primer resultado frente a repetir el efecto.
- Fallo al actualizar caché: conservar el valor anterior frente a dejar estado parcial.
- Estilo no soportado: devolver un error distinguible frente a producir un resultado parcial.
Cada diferencia merece un caso si importa al consumidor. No merece tres casos con datos distintos si todos rechazan exactamente la misma alternativa.
Tres preguntas para revisar un test generado#
Usa estas preguntas en el PR:
- Contraste: ¿qué implementación incorrecta pero plausible hace fallar este test?
- Independencia: ¿esa alternativa existía antes de leer el diff o la dedujimos de la propia solución?
- Libertad: ¿otra implementación correcta, con distinta estructura interna, seguiría pasando?
Si no podemos responder la primera, el test probablemente demuestra una trivialidad. Si falla la segunda, código y prueba pueden estar repitiendo la misma invención. Si falla la tercera, el test está fijando el interior en vez del contrato.
Prompt breve para obtener el contraste#
Antes de pedir tests o implementación, añade:
Para cada decisión observable de esta tarea:
1. Propón dos comportamientos incompatibles pero plausibles.
2. Indica cuál está confirmado por el repositorio o la documentación.
3. Si ninguno está confirmado, formula una pregunta y no decidas por probabilidad.
4. Para el comportamiento confirmado, escribe el caso mínimo que haga fallar la alternativa.
5. Explica qué mutación concreta debe hacer fallar ese test.
No uses detalles de la implementación propuesta para definir el contraste.El objetivo no es producir más tests. Es conseguir que cada test nuevo pueda nombrar la decisión que protege y la alternativa que rechaza.
Verde no significa verificado. Primero demuestra que el caso sabe reconocer una decisión equivocada.