Hace unos años heredé una suite de regresión con más de doscientos casos en verde. Impecable en el reporte.

Al mirarla de cerca, buena parte de los valores esperados se habían generado de la forma más práctica del mundo: corrieron el sistema, vieron qué devolvía, y guardaron eso como el resultado correcto. El equipo lo llamaba “fijar el baseline”. Tenía sentido para ellos: nadie tenía la especificación completa y el sistema llevaba años en producción, así que lo que hacía se asumía bueno.

Esa suite no comprobaba que el sistema funcionara. Comprobaba que siguiera haciendo lo mismo que el día que fijaron el baseline. Los bugs que ya existían ese día quedaron adentro, en verde, protegidos por un test que los defendía.

Un test que compara el sistema contra sí mismo no es un oráculo. Es una fotocopia con permiso para decirte que todo está bien.

Doscientos casos inútiles por no responder una pregunta de una línea:

¿De dónde sacaste el valor esperado?

En la primera parada de esta serie te dije que cada assert que escribes es un goal verificable. Lo sostengo, y hoy lo completo: el assert no es el oráculo, es el renglón donde lo escribes. El oráculo es la respuesta a esa pregunta.

Oráculo, en cristiano

Si es la primera vez que ves la palabra, no te compliques: el oráculo es de dónde sacas la respuesta correcta contra la que vas a comparar. Cuando ejecutas un caso a mano y en la columna “resultado esperado” escribes el saldo queda en 90.000, lo que te hizo escribir ese número —la regla de negocio, la historia de usuario, la calculadora— es el oráculo. La celda es solo donde lo anotaste.


De dónde puede salir “correcto”

El valor esperado tiene que venir de algún lado, y las opciones son pocas. Elegir bien entre ellas es más de la mitad del trabajo de diseñar un caso.

De dónde sale el esperadoCómo se llamaEjemplo
La regla de negocio o el criterio de aceptaciónEspecificado”El IVA es el 10%“
Una regla que se cumple siempre, sin saber el datoDe propiedad”La lista viene ordenada”
Otra fuente independienteDe consistencia”El sistema viejo daba lo mismo”
Un límite acordado sobre muchas ejecucionesDe umbral”El p95 por debajo de 500 ms”
El criterio de una personaHumano”Así no se entiende”

El primero es el más fuerte y el que deberías buscar siempre. El último no escala, pero es el único que sirve cuando el criterio todavía no está escrito en ningún lado.

Y falta la fila que parece un oráculo y no lo es: la salida anterior del propio sistema, guardada como referencia. Snapshot, baseline, golden master. Sirve para detectar cambios que nadie quiso hacer, pero no puede decirte si el comportamiento es correcto, porque nunca supo qué era correcto.

Cómo saber en dos minutos qué tienes

Agarra tres casos de tu suite y pregúntate, por cada valor esperado, de dónde salió. Si la respuesta es “de la historia de usuario”, tienes un oráculo. Si es “de correr el sistema y copiar”, tienes un snapshot disfrazado. Si es “no sé, ya estaba”, tienes un test que nadie puede defender cuando se ponga rojo.


Cuando no puedes conocer el valor exacto

Acá es donde la mayoría se rinde y se va al snapshot. El listado depende de los datos de hoy, el ID lo genera el backend, el precio cambia con la cotización.

Hay una salida mejor: si no puedes afirmar cuánto, afirma qué tiene que cumplirse siempre.

test("el listado de facturas viene ordenado y sin repetidos", async ({
request,
}) => {
const { facturas } = await (
await request.get("/api/facturas?orden=fecha")
).json();
// No sé qué facturas va a haber hoy. Sí sé qué tiene que ser cierto siempre.
const fechas = facturas.map((f) => new Date(f.fecha).getTime());
expect(fechas).toEqual([...fechas].sort((a, b) => a - b));
const ids = new Set(facturas.map((f) => f.id));
expect(ids.size).toBe(facturas.length);
});

Ese test no sabe cuántas facturas hay ni cuáles son, y aun así es un oráculo de verdad: si el backend rompe el orden o duplica un registro, se pone rojo. Y sigue sirviendo el mes que viene, con datos completamente distintos. Eso es un oráculo de propiedad.

Las propiedades que más uso:

  • Dominio cerrado — el campo estado solo puede ser uno de cinco valores.
  • Orden — lo que dice estar ordenado, está ordenado.
  • Unicidad — no hay identificadores repetidos.
  • Conservación — el total del carrito es la suma de sus ítems; después de mover plata de una cuenta a otra, el saldo total del sistema no cambió.
  • Ida y vuelta — lo que exportas y vuelves a importar tiene que dar lo mismo.
  • Ausencia — en la respuesta no aparece un dato que no debería estar nunca.

Esa última es la que más bugs caros me encontró: no verifica lo que el sistema hace bien, verifica lo que jamás tiene permitido hacer.


Asertar sobre lo que escribió un modelo

Si tu equipo metió IA en el producto —un resumen, una clasificación, una respuesta al cliente— tienes el problema anterior en su versión difícil: la salida cambia cada vez que la pides. Comparar contra un texto exacto no sirve, porque mañana lo redacta distinto y tu test se cae sin que haya ningún bug. Y aflojar hasta expect(respuesta).toBeTruthy() es peor: queda verde con cualquier cosa.

El camino es el mismo de recién, con otra piel.

const informe = await agente.analizarIncidente(ticket);
// La redacción cambia siempre; estas cuatro cosas no pueden cambiar nunca.
expect(informe.severidad).toMatch(/^(baja|media|alta|critica)$/); // dominio cerrado
expect(informe.pasosParaReproducir.length).toBeGreaterThan(0); // completitud
expect(informe.texto).toContain(ticket.id); // trazabilidad al origen
expect(informe.texto).not.toMatch(/\d{4}-\d{4}-\d{4}-\d{4}/); // sin tarjetas

Cuatro assertions que no le piden al modelo escribir siempre lo mismo, y aun así lo tienen agarrado del cuello: formato válido, respuesta completa, atada al ticket que la originó y sin datos sensibles adentro.

Sobre pedirle a otro modelo que califique

Existe la práctica de usar un segundo modelo como juez de la salida del primero, y en algunos casos sirve. Ojo con dos cosas: un juez probabilístico hoy aprueba y mañana no, así que tu suite hereda esa inestabilidad; y si el juez es el mismo modelo que produjo la respuesta, volviste al eco. Cuando el criterio se puede escribir como regla, escríbelo como regla. Eso es lo que hacen los hooks como quality gates deterministas.


Las tres formas de romper la independencia

En la primera parada dejé una regla: el que ejecuta no puede ser el que califica. Enunciada así suena obvia. En el código se rompe todo el tiempo, casi nunca a propósito.

El esperado se calcula con la misma función que estás probando.
El esperado sale de una consulta al mismo sistema bajo prueba.
El snapshot se actualiza para que el test vuelva a pasar.

La primera es la más silenciosa:

// Roto: el esperado sale de la misma función que estoy evaluando.
// Si calcularIva tiene un bug, el test lo acompaña y queda verde.
expect(calcularIva(monto)).toBe(calcularIva(monto));
// Sano: el esperado sale de la regla de negocio, escrito a mano.
expect(calcularIva(100_000)).toBe(10_000); // IVA 10%, régimen general

La tercera es la más común, y duele porque tiene forma de tarea de mantenimiento. El test se pone rojo, alguien mira el diff por encima, no le parece grave y actualiza la referencia. Ahí el sistema acaba de aprobar su propio cambio, y tu suite lo firmó.

Una regla de equipo que funciona

Actualizar un snapshot no es mantenimiento: es una decisión de producto. Alguien está declarando que el comportamiento nuevo es el correcto. Que quede escrito con esa frase en el pull request y que lo firme quien tiene el criterio. El día que el cambio no sea intencional, alguien lo va a frenar.


Las cuatro preguntas antes de escribir el assert

Este es el takeaway. Cuatro preguntas por cada goal, en este orden. La primera descarta la mitad de los tests malos antes de nacer.

  1. ¿De dónde sale el valor esperado? De la especificación o del criterio de aceptación, vas bien. De correr el sistema, tienes una foto de cómo se comporta hoy, con los bugs de hoy adentro.

  2. ¿Puedo escribirlo antes de ejecutar? Un goal verificable se redacta sin haber corrido nada. Si necesitas ver la salida para saber qué esperar, todavía no tienes criterio: tienes curiosidad. Explorar así está perfecto, pero eso es una sesión exploratoria, no un caso automatizado.

  3. ¿Falla por la razón correcta? Si rompo la regla que este caso protege, ¿se pone rojo? ¿Y si rompo otra cosa, se queda quieto? Es lo que mide de verdad el mutation testing: no cuánto código tocaste, sino si tus assertions se enteran cuando algo cambia.

  4. ¿Quién lo mantiene cuando la regla cambie? Deja rastro de dónde salió el número, aunque sea en un comentario, para que quien lo toque dentro de un año sepa si está arreglando un test o cambiando una política.

Esa pregunta 3 es la que le pone contenido a la pieza 4 del harness, la que desarmamos en la anatomía: no alcanza con que el oráculo exista en el framework, tiene que venir de afuera y ponerse rojo por el motivo correcto.


Lo que quiero que te lleves

Un goal verificable no se define por su sintaxis. Puedes escribir un expect perfecto y no tener oráculo, o tener uno sólido en una assertion de tres palabras. La diferencia está afuera del renglón: de dónde salió el criterio, y si es independiente de lo que estás evaluando.

Esa distinción lleva décadas discutiéndose en testing, y es con la que el mundo de los agentes está tropezando ahora con nombre nuevo. Cuando alguien te pregunte cómo hacer que una IA sepa si terminó bien, ya tienes la respuesta: dale un criterio que no se lo haya inventado ella.

Especificado El más fuerte, cuando existe la regla
De propiedad Cuando el valor cambia y la regla no
Independiente Nunca del sistema que evalúas

La próxima parada baja al teclado: el harness de UI/E2E, del cero al primer flujo verde. Vas a armar las seis piezas con Playwright, y ese primer expect que escribas ya no va a ser un renglón cualquiera — vas a saber de dónde tiene que salir.