Al terminar esta lección
- Trazar una regla de negocio hasta un test y una evidencia sin saltos.
- Elegir el nivel de prueba según el mecanismo de fallo y no según la capa de la arquitectura.
- Diseñar un oráculo que verifique el resultado, los efectos presentes y los efectos ausentes.
- Distinguir una prueba que defiende una propiedad de una que solo ejecuta código.
10. Prometimos comportamiento, ahora hay que demostrarlo
Testing no entra porque "todo proyecto profesional tiene tests". Entra porque prometimos comportamiento a alguien: al dueño de Nexo, que no quiere vender unidades que no existen; a administración, que no quiere débitos que no corresponden; a un proveedor fiscal externo que puede responder tarde o no responder; a dos vendedores que pueden tocar la última unidad de AX-442 en el mismo segundo.
La pregunta correcta no es cuánta cobertura tenemos. La pregunta es qué evidencia justifica creer que cada promesa sobrevive a errores, fronteras, fallos externos y concurrencia. Si una promesa no tiene un test que se ponga rojo cuando se rompe, esa promesa no está demostrada, aunque la suite tenga cientos de tests verdes.
10.1 Requerimiento, escenario, caso de prueba, test y evidencia
Problema. Un equipo escribe tests porque "hay que tener tests" y termina con una suite grande que no responde ninguna pregunta concreta. Cuando alguien pregunta "¿cómo sabemos que no hay sobreventa?", nadie puede señalar un test. La suite existe, pero no está conectada con lo que se prometió al cliente.
Definición operativa. La cadena tiene cinco eslabones y cada uno es un artefacto distinto. No se pueden fusionar sin perder trazabilidad:
- Requerimiento: una regla o requisito con su frontera explícita. En Nexo, RN-05: la disponibilidad por depósito y SKU nunca puede quedar por debajo de cero.
- Escenario o criterio de aceptación (AC): la forma observable de la regla. AC-06: con una unidad y dos reservas concurrentes, exactamente una confirma.
- Caso de prueba (T): preparación, estímulo y oráculo, todavía sin código. T06: stock = 1 en el depósito CBA para AX-442, dos requests distintos, resultado esperado exactamente una reserva y disponibilidad final 0.
- Test: el código ejecutable que implementa el caso. En T06 es un integration test con dos conexiones a PostgreSQL real y una barrera que las obliga a competir.
- Evidencia: la salida reproducible de la ejecución, asociada a un commit, a un entorno y a un conjunto de datos conocido.
El eslabón que más se saltea es el tercero. Sin caso de prueba escrito en lenguaje de negocio, el test se diseña mirando la implementación y termina verificando lo que el código hace, no lo que el negocio necesita.
Ejemplo en Nexo. El equipo recibió la pregunta del depósito y siguió la cadena hacia atrás. RN-05 define la invariante. AC-06 la convierte en un escenario concurrente. T06 define qué se prepara, qué se ejecuta y qué resultado es correcto. El test implementa T06 contra la misma base que usa producción. La evidencia de CI queda atada al commit que se probó. Cualquier persona puede recorrer la cadena en menos de un minuto.
Contraejemplo. Existe un test llamado "reserva stock" que mockea el repositorio y verifica que reserve fue llamado con los parámetros correctos. Está verde, tiene nombre claro y aparece en el informe de CI. No defiende RN-05. Aunque el repositorio real tuviera una carrera, el mock nunca la ejecuta. El problema no es que el test sea malo: es que no puede fallar por el defecto que nos preocupa, y eso lo deja fuera de la cadena.
Cómo se verifica. Pedile a alguien del equipo que, para un test cualquiera, señale la RN que defiende, el AC, el caso T y la línea de evidencia. Si falta un eslabón, la cadena está rota. El mismo control funciona en sentido inverso: si un test falla porque la regla estaba mal formulada, la corrección vuelve a RN o AC. No se ajusta el test para que quede verde. Ese es el momento en que el test cumplió su función: obligó a decidir qué debía pasar.
Ordená la cadena desde la regla hasta la evidencia
Usá las flechas para dejar el orden correcto de los eslabones.
- Posición 1 de 5: Definir T06: preparación stock 1, dos requests y oráculo
- Posición 2 de 5: Ejecutar en CI y guardar la salida asociada al commit
- Posición 3 de 5: Leer RN-05 y su frontera
- Posición 4 de 5: Traducirla a AC-06 con un escenario concurrente
- Posición 5 de 5: Escribir el integration test con dos conexiones y barrera
10.2 Elegir el nivel por el mecanismo de fallo
Problema. La pirámide de tests se usa como ley: muchos unit, algunos integration y pocos E2E. Eso lleva a dos errores opuestos. El primero es poner todo en unit con mocks, lo que vuelve invisibles las propiedades que dependen de la base, del orden de las transacciones o de un tercero. El segundo es subir todo a E2E: son lentos, frágiles y muchas veces no observan la regla específica que querías defender.
Definición operativa. El nivel correcto es el más barato que todavía puede observar el mecanismo que podría romper la propiedad. Para elegirlo respondé tres preguntas: qué fallo querés detectar, qué parte del sistema lo produce y qué observador lo puede ver. Si el observador es un mock, el fallo que vive dentro del mock no existe para la prueba.
Estos son los niveles que usamos en Eje Z y qué mecanismo observa cada uno:
- Unit: lógica pura e invariantes de dominio. En Nexo, la comparación de saldo más pedido contra el límite de crédito, expresada en centavos enteros, es un caso típico: T02 y T03 como regla. Lo que no puede ver: transacciones, constraints y escrituras concurrentes.
- Integration: transacciones, constraints, SQL y adaptadores con infraestructura real. T09 (rollback si falla antes del commit) necesita la base real porque el rollback es un comportamiento de la base, no de nuestro código. Lo que no puede ver: lo que hace un sistema externo que no levantamos.
- Contract: fronteras con APIs externas o públicas. Valida payloads, errores, timeouts e idempotencia de la traducción que hace nuestra aplicación. Lo que no puede ver: cambios del proveedor real que el stub no conoce todavía.
- E2E: objetivos del actor con el sistema completo. Pocos, elegidos por riesgo y valor: el vendedor crea un pedido desde la interfaz y ve su estado. Lo que no puede ver: la causa interna de un error, que aparece como una pantalla genérica.
- Concurrente: propiedades que solo se rompen bajo carreras. T06 es el ejemplo central. Requiere la misma tecnología de base y dos ejecuciones simultáneas controladas.
- Regresión: evidencia de que un defecto ya pagado no vuelve. Su nivel depende de dónde estuvo la causa: una regla de redondeo se reproduce con unit; una transacción parcial, con integración.
Asigná el nivel mínimo que puede observar cada mecanismo
Elegí el nivel más barato que todavía puede detectar el fallo descripto.
El cálculo del total usa redondeo bancario en lugar de half-up en céntimos.
Una falla después de insertar el pedido deja la reserva a medio escribir.
El proveedor responde 200 con un cuerpo que no trae el identificador de comprobante.
Dos transacciones leen stock 1 al mismo tiempo y ambas reservan la última unidad.
Contraejemplo. El equipo escribió un test de concurrencia con un repositorio en memoria protegido por un mutex. Pasa siempre, porque el mutex serializa todo. Lo que parecía un test de carrera es un test de secuencia. La prueba debe correr contra la misma tecnología y el mismo aislamiento que producción, porque un fake puede eliminar exactamente el comportamiento que queremos observar.
Cómo se verifica. Para cada riesgo de la matriz, anotá el nivel elegido y la razón por la que un nivel más barato no puede observarlo. Si la respuesta es "porque es más lento", no alcanza: la lentitud se resuelve separando suites, no bajando el nivel. Si la respuesta es "porque necesitamos el comportamiento real de la base", el nivel es integration o concurrente.
10.3 Matriz de pruebas de CU-01
Problema. Sin un artefacto que resuma la estrategia, cada persona del equipo tiene una idea distinta de qué está probado. La matriz de CU-01 reúne doce casos, cada uno ligado a un riesgo o frontera, a una preparación concreta, a un resultado observable y a un nivel. Es el primer documento que se le muestra a un mentor o a un cliente cuando pregunta por la calidad del slice.
Definición operativa. Cada fila es un caso de prueba. La columna de nivel dice qué observador lo ejecuta. La columna AC conecta cada caso con el catálogo de criterios de aceptación. Una matriz bien hecha tiene menos filas que tests existen, porque algunos tests cubren más de un caso y algunos casos no necesitan test automatizado (se resuelven con revisión).
| ID | Riesgo / frontera | Preparación | Resultado esperado | Nivel | AC / RN |
|---|---|---|---|---|---|
| T01 | happy path | cliente estándar, saldo más pedido por debajo del límite, stock suficiente. | pedido confirmado, precio snapshot, reserva creada. | unit + integration + E2E | AC-01, AC-04 / RN-03 |
| T02 | exactamente en el límite | saldo más pedido igual al límite. | aprobado según RN-01. | unit | AC-02 / RN-01 |
| T03 | un centavo sobre el límite | cliente estándar, saldo más pedido superior al límite en un centavo. | rechazo por crédito, sin pedido ni reserva ni débito. | unit + integration | AC-03 / RN-01 |
| T04 | autorización de crédito | política que exige aprobación. La tolerancia Platino no se fija en el test (RN-02 BLOCKED). | PENDIENTE_AUTORIZACION y registro de la solicitud. | integration | AC-08 / CU-02 |
| T05 | SKU inválido | un ítem del pedido no existe en el maestro. | rechazo por SKU, sin pedido parcial ni reservas. | integration | AC-05 |
| T06 | última unidad concurrente | stock = 1 en CBA; dos requests distintos sincronizados con barrera. | exactamente una reserva; la otra recibe stock insuficiente; disponibilidad final 0. | integration concurrente | AC-06 / RN-05 |
| T07 | idempotencia con el mismo contenido | mismo requestId y mismo payload, incluida la pérdida de la respuesta después del commit. | mismo resultado y un único pedido. | integration | AC-07 / RN-06 |
| T08 | idempotencia conflictiva | mismo requestId con payload distinto. | conflicto (409), sin segundo efecto. | integration | AC-07 / RN-06 |
| T09 | falla al persistir | error inyectado antes del commit. | rollback completo: sin pedido, sin líneas, sin reservas, sin registro de idempotencia. | integration | AC-10 |
| T10 | actor sin permiso | vendedor fuera de su cartera intenta operar un cliente ajeno. | 403, sin filtrar datos, intento auditado. | API / integration | NFR-03 (sin AC propio) |
| T11 | transición ilegal | pedido ya despachado recibe una operación de confirmación. | rechazo de la operación, datos intactos. | unit + integration | máquina de estados de CU-01 (sin AC propio) |
| T12 | proveedor externo caído | pedido ya committeado y dependencia externa sin respuesta. | el pedido permanece válido y la operación externa queda reintentable. | integration / contract | AC-12 / NFR-04 |
Leé la última columna con atención, porque es la que hace útil la matriz. Diez de los doce AC del catálogo tienen al menos una fila. Dos no la tienen: AC-09 (cantidad inválida y múltiplos de venta, RN-08) y AC-11 (handoff a preparación por outbox). Esa no es una falla de la matriz; es información. Una RTM honesta deja esos huecos visibles como pendientes. Lo que no hacemos es escribir una fila cualquiera para que la tabla quede completa.
También hay una decisión que el lector tiene que notar: T04 no fija ninguna tolerancia Platino. RN-02 sigue BLOCKED porque el porcentaje o monto no está validado. El test verifica que el pedido queda pendiente de autorización, no un número que nadie aprobó.
10.4 El oráculo incluye la ausencia de efectos
Problema. Un test de error suele terminar en expect(status).toBe(422). Eso verifica que el sistema respondió algo parecido a un rechazo, pero no verifica que el rechazo haya sido limpio. Un rechazo por crédito puede haber dejado una reserva persistida, un débito a medio aplicar o un pedido en estado borrador. Todos esos efectos pasan desapercibidos si el oráculo solo mira la respuesta.
Definición operativa. El oráculo es el criterio que decide si una prueba pasó. Un oráculo completo para una operación con efectos tiene cuatro partes:
- El resultado observable: el tipo de resultado y su razón.
- Los efectos que deben existir: el pedido de un éxito, la reserva, el registro de idempotencia.
- Los efectos que no deben existir: pedido parcial, reserva huérfana, débito, evento duplicado.
- El estado final de los datos, que es lo que quedó persistido y no lo que el handler dijo que hizo.
En sistemas de negocio, la segunda y la tercera parte son el contrato. Un rechazo que deja efectos es un defecto aunque la respuesta sea la correcta.
Ejemplo en Nexo. RN-01 dice que un cliente estándar no puede confirmar si saldo más pedido supera el límite. El caso de frontera es el centavo. Con saldo de 480.000 y límite de 500.000, un pedido de 20.000 cabe justo (T02) y un pedido de 20.000,01 no entra (T03). Este es el test de T03 en el formato del curso:
import { test } from "node:test";
import assert from "node:assert/strict";
import { fixture } from "../support/fixture";
test("T03: un centavo sobre el límite rechaza sin efectos (AC-03, RN-01)", async () => {
const env = await fixture({
client: { id: "CLI-0042", tier: "estandar", balanceCents: 48_000_000, creditLimitCents: 50_000_000 },
stock: { "AX-442": { CBA: 10 } },
priceCents: { "AX-442": 2_000_001 },
});
const result = await env.createOrder({
requestId: "req-501",
sellerId: "SEL-01",
clientId: "CLI-0042",
deliveryMethod: "pickup",
items: [{ sku: "AX-442", quantity: 1 }],
});
assert.deepEqual(result, { kind: "rejected", reason: "credit" }); // resultado
assert.equal(await env.countOrders("req-501"), 0); // no existe pedido
assert.equal(await env.reservedUnits("AX-442", "CBA"), 0); // no hay reserva
assert.equal(await env.balanceCents("CLI-0042"), 48_000_000); // no hubo débito
});
La suma es 48.000.000 + 2.000.001 = 50.000.001, un centavo por encima del límite de 50.000.000. El fixture es un helper de tests/support que arma los datos y expone consultas. Lo importante no es el helper sino las cuatro últimas aserciones: cada una cubre un efecto que no debe ocurrir.
Oráculo débil frente a oráculo completo para T03
Solo verifica que el handler respondió 422 o un resultado de rechazo. Si el código reservaba stock antes de evaluar crédito y lo liberaba después, el test pasa igual. Tampoco detecta que el saldo cambió.
Verifica el resultado con su razón, que no exista pedido, que la reserva sea cero y que el saldo no se haya tocado. Si el código deja cualquier efecto parcial, el test se pone rojo en la línea que lo muestra.
Contraejemplo. Una versión anterior del caso de uso reservaba stock antes de evaluar el crédito y devolvía rechazo después, sin deshacer la reserva. El test con solo status 422 pasaba, porque la respuesta era la esperada. El test con reservedUnits lo detectaba en la primera ejecución. La diferencia no era cobertura: era qué efectos observaba el oráculo.
Cómo se verifica. Para cada caso con efectos, escribí dos listas: lo que debe quedar y lo que no debe quedar. Después revisá el test y comprobá que cada elemento de la segunda lista tenga una aserción. Un buen truco es ejecutar el test contra una versión a propósito defectuosa (ver 10.5) y mirar si la falla aparece en la línea correcta.
10.5 Dobles de prueba: cuándo mienten
Problema. Los dobles hacen rápidas las pruebas y permiten aislar un componente. El riesgo aparece cuando el doble reemplaza el comportamiento que la prueba debía defender. En ese caso la prueba no falla nunca por el defecto que nos preocupa, porque el defecto vive en la parte que fue reemplazada.
Definición operativa. Usamos tres términos con sentido preciso:
- Stub: devuelve respuestas fijas. Sirve para simular una dependencia que responde algo conocido, por ejemplo un proveedor de crédito que devuelve siempre "aprobado".
- Mock: además de responder, registra llamadas y permite verificarlas. Es útil cuando el contrato es la interacción (por ejemplo, que se llame una sola vez al proveedor), no el resultado.
- Fake: una implementación simplificada pero con comportamiento real, por ejemplo un repositorio en memoria que respeta las mismas reglas de unicidad. Sirve para lógica de aplicación, pero no para propiedades de la base.
La regla de Eje Z es que el doble debe ser fiel al comportamiento que el test defiende. Un doble que responde siempre lo mismo no puede fallar. Un fake que serializa todas las operaciones no puede mostrar una carrera. Y un mock que verifica llamadas no dice nada sobre si la base aceptó la escritura.
Ejemplo en Nexo. Un equipo tenía un test "rechaza reserva sin stock" con un repositorio mock que devolvía available: 0. Pasaba. Tenía otro test "reserva cuando hay una unidad" con available: 1. Pasaba. Ninguno de los dos ejecutaba dos transacciones simultáneas. Los dos tests eran correctos sobre el código que tocaban. Lo que no tocaban era el mecanismo que podía fallar en producción: la lectura y la escritura separadas de stock.
Contraejemplo. El caso más peligroso es el mock que decide el resultado. Si el test fija available: true y espera 422, el mock empuja la decisión hacia el camino que no queremos probar. Si el test espera 201 y el mock devuelve true, el test verifica el camino feliz sin el stock real. En ambos casos, borrar la validación de stock del caso de uso deja el test verde. Esa es la prueba más simple de un oráculo débil: si eliminar la regla no rompe el test, el test no defiende la regla.
Cómo se verifica. Para cada doble, respondé una pregunta: si el componente real tuviera el defecto que nos preocupa, ¿el doble lo haría visible? Si la respuesta es no, el doble debe reemplazarse por la infraestructura real para ese riesgo. Los dobles siguen siendo útiles para todo lo demás: el costo es que cada uno tiene que decir qué riesgo deja fuera.
10.6 Contract tests: defender la traducción a un tercero
Problema. Nuestra aplicación no controla la API del proveedor fiscal. Puede cambiar el formato de un campo, responder con timeout o devolver errores nuevos. Si nuestro código asume un formato que ya no existe, el fallo aparece en producción, lejos de la causa, con pedidos ya confirmados.
Definición operativa. Un contract test valida la traducción que nuestra aplicación asume: qué campos son obligatorios, qué errores son transitorios y cuáles permanentes, qué significa un timeout, qué semántica tiene la clave de idempotencia. No necesita pegarle siempre a producción. Puede ejecutarse contra un stub que reproduzca el contrato o contra un sandbox del proveedor, según lo que esté disponible.
Un contrato mínimo se puede expresar como tipo y validación en el borde del adaptador:
import { z } from "zod";
export const FiscalEmitOk = z.object({
status: z.literal("ok"),
cae: z.string().min(1),
voucherNumber: z.string().min(1),
});
export const FiscalEmitRejected = z.object({
status: z.literal("rejected"),
code: z.string(),
permanent: z.boolean(),
});
export type FiscalEmitResponse =
| z.infer<typeof FiscalEmitOk>
| z.infer<typeof FiscalEmitRejected>
| { status: "unknown" }; // timeout o corte: no sabemos si se emitió
El tercer caso es el importante. unknown no es un error más: significa que el efecto externo puede haber ocurrido. Un adaptador que traduce un timeout a "falló, reintentar" puede duplicar un comprobante. Un contract test defiende esa diferencia.
Ejemplo en Nexo. El primer release no emite comprobantes: el pedido queda marcado como pendiente de facturación, y la integración fiscal queda fuera de alcance del primer corte. Aun así, T12 define que un proveedor caído no invalida un pedido ya confirmado, y el equipo diseña dos contract tests para que la etapa de integración no empiece con un contrato asumido. Son los que la práctica E10 pide: timeout y respuesta inválida.
Contraejemplo. Un mock escrito por el mismo equipo que siempre devuelve { status: "ok", cae: "123" }. Pasa siempre, y solo demuestra que el equipo conoce su propio mock. No detecta que el proveedor cambió el nombre del campo, que devuelve 200 con un error en el cuerpo ni que su timeout no garantiza que la operación se haya cancelado.
Cómo se verifica. Para cada integración externa, escribí una lista de respuestas que el adaptador debe manejar: éxito, rechazo permanente, rechazo transitorio, timeout antes de procesar, timeout después de procesar, respuesta sin campos obligatorios. Si el adaptador no tiene un test para cada respuesta, el contrato es una suposición.
10.7 Concurrencia: ejecución real o no hay carrera
Problema. Una carrera es un defecto que depende del orden de dos operaciones. Probarla ejecutando una operación y después la otra no dice nada: el orden siempre es el mismo y el defecto nunca se manifiesta. El equipo tiene que lograr que las operaciones compitan de verdad.
Definición operativa. Una prueba concurrente tiene tres componentes. Primero, dos o más operaciones sobre el mismo estado y con la misma tecnología que producción. Segundo, una barrera que obliga a todas a empezar cuando están listas. Tercero, un oráculo que mire el estado final: exactamente un resultado de éxito y la invariante persistida.
export function barrier(parties: number) {
let arrived = 0;
let release!: () => void;
const open = new Promise<void>(resolve => (release = resolve));
return {
async wait() {
if (++arrived === parties) release();
await open;
},
};
}
Ejemplo en Nexo. T06 usa exactamente esa barrera con dos conexiones a PostgreSQL. Las dos transacciones empiezan, esperan la barrera y después intentan reservar la última unidad de AX-442 en CBA. Una de las dos bloquea la fila mientras la otra espera su commit. Cuando commitea, la segunda reevalúa la condición y no encuentra disponibilidad. El detalle técnico se desarrolla en 10.2; en este nivel importa que la prueba obligó a que las operaciones compitieran.
Contraejemplo. Dos llamadas con await seguidas. Parece concurrencia y no lo es: la segunda empieza después de que la primera terminó. Tampoco alcanza con Promise.all sobre funciones que no tienen ningún punto de sincronización: el orden de inicio puede ser casi siempre el mismo y la carrera aparece en una de cada mil ejecuciones.
Cómo se verifica. Una prueba concurrente que pasa una vez no demuestra nada. Repetila el número de veces que el riesgo justifique y guardá cuántas ejecuciones hiciste, porque esa cantidad es parte de la evidencia. Si la prueba pasa cien veces, no demostraste que no hay carrera: demostraste que no la encontraste con ese arnés. Por eso el mecanismo de fondo (una actualización condicional que la base resuelve de forma atómica) es más confiable que cualquier cantidad de repeticiones.
10.8 Carga y seguridad: solo cuando el requisito existe
Problema. Alguien ejecuta una consulta en su notebook, ve que tarda 120 ms y lo presenta como "el NFR cumple". Ese número no valida nada: depende del hardware, de los datos, de que la base esté fría o caliente y de cuántas personas la usan al mismo tiempo.
Definición operativa. Un NFR medible tiene siete elementos que deben quedar escritos antes de medir: el escenario, el entorno, el dataset, el patrón de carga (usuarios, rampa y pausa entre acciones), el calentamiento, la duración y el percentil con su umbral. Sin uno de ellos, el número no es un resultado sino una anécdota.
En Nexo el requisito es NFR-01: el p95 del registro de pedido, excluyendo el servicio fiscal, debe ser menor a 800 ms con 30 usuarios concurrentes. El umbral está en el requisito; la medición todavía no existe. La especificación no inventa cifras antes de medir, pero sí fija el criterio que la medición tiene que respetar. En 10.3 se diseña el plan de medición completo.
Seguridad con el mismo criterio. No se prueba una lista infinita de ataques. Se prueban los permisos y los límites de contexto que el slice necesita. En CU-01 eso es NFR-03: un vendedor solo opera los clientes de su cartera, y todo intento denegado queda auditado. T10 lo verifica con un vendedor fuera de cartera. El test no busca todas las formas de burlar la autorización; busca la que el negocio realmente necesita impedir.
Cómo se verifica. Para un NFR, revisá si el plan de medición permitiría que otra persona repitiera el resultado con el mismo entorno y los mismos datos. Si la respuesta depende de "mi máquina", el requisito no está verificado.
Errores frecuentes
- Síntoma: la suite está verde, pero un bug de stock llega a producción igual. Causa: el test de stock mockea el repositorio, así que nunca ejecuta la base. Corrección: para el riesgo de stock, el test necesita PostgreSQL real y, si el riesgo es concurrente, dos conexiones con barrera.
- Síntoma: el test de rechazo pasa aunque la lógica de crédito cambie. Causa: el oráculo solo mira el status HTTP y el mock de crédito decide el resultado. Corrección: agregá aserciones de efectos ausentes y verificá que el test falle si eliminás la regla.
- Síntoma: el test de concurrencia pasa siempre, incluso con el código ingenuo. Causa: las operaciones se ejecutan en secuencia o el repositorio en memoria serializa todo. Corrección: barrera real, misma base y prueba de la prueba: el test debe ponerse rojo contra la versión defectuosa.
- Síntoma: el esperado del test cambió después de correr el código. Causa: el oráculo se tomó de la salida de la implementación y no de la especificación. Corrección: volvé a la RN o AC. Si la regla estaba mal, corregís la regla; si el código estaba mal, corregís el código. Nunca el expected sin decidir cuál es.
- Síntoma: el informe dice "cobertura 95 %" y nadie puede decir qué riesgo está cubierto. Causa: la cobertura se usó como meta en lugar de como señal. Corrección: armá la matriz de riesgos y usá la cobertura solo para encontrar zonas sin ejercitar.
Qué puede hacer el agente: derivar casos frontera a partir de RN, generar esqueletos de test, buscar ramas de riesgo sin ejercitar y resumir resultados de ejecución.
Qué no debe decidir: el oráculo cuando la regla de negocio es ambigua, si la suite es suficiente, o si un mock puede reemplazar la integración de un riesgo crítico.
Contexto mínimo útil: RN, CU y AC relevantes, el mapa de riesgos, los archivos del adaptador bajo prueba y los comandos reales de ejecución.
Prompt de ejemplo:
Contexto: CU-01 de Nexo, RN-01 (crédito estándar), RN-05 (stock no negativo), AC-03 y AC-06.
Tarea: para cada caso, proponé un test node:test con oráculo completo. Incluí el resultado esperado, los efectos que deben existir y los efectos que no deben existir.
Restricciones: no uses mocks para el repositorio de stock; no fijes ninguna tolerancia Platino; no inventes códigos de error que no estén en el contrato.
Salida: un archivo por caso, y para cada test, una línea que diga qué defecto detecta y qué defecto no detecta.
Cómo revisar la salida: pedí que explique qué defecto detecta cada test y cuál no. Después eliminá a propósito la regla en el código y ejecutá la suite. Si algún test sigue verde, el oráculo es débil. Ejecutá los tests en el entorno con PostgreSQL real, no en el entorno del agente.
Glosario de la lección
Vocabulario de testing para CU-01
Frente
Requerimiento
Para llevarte
- La cadena RN → AC → caso T → test → evidencia tiene que poder recorrerse sin saltos. Si falta un eslabón, la afirmación de que el comportamiento está probado no se sostiene.
- El nivel de prueba se elige por el mecanismo de fallo: el más barato que todavía puede observar la propiedad.
- Un oráculo completo verifica resultado, efectos presentes y efectos ausentes. En operaciones con efectos, lo que no debe ocurrir es parte del contrato.
- Un doble tiene que ser fiel al riesgo que la prueba defiende. Un mock que decide el resultado vuelve invisible el defecto.
- Los contract tests defienden la traducción a terceros, sobre todo los estados ambiguos como un timeout con efecto posible.
- Un NFR sin entorno, dataset y criterio escritos no es un resultado. Cobertura alta tampoco es confianza: es una señal secundaria.
¿Cómo valoras el contenido técnico de esta lección?
Tu feedback nos ayuda a mejorar los laboratorios prácticos de Eje Z.