Cualquier ingeniero desconfía de un build rojo. El artefacto peligroso es el verde, porque el verde transporta una afirmación implícita: esto se midió, y aguantó. Esa afirmación tiene dos modos de fallo y solo uno es famoso. Un test puede estar equivocado sobre el sistema. Un test también puede tener razón sobre la nada: apuntado a un artefacto que nadie publica, a un archivo que nadie carga, a una promesa que nadie corre. Pasa para siempre, y no protege nada.
Pasamos una temporada coleccionando especímenes del segundo tipo, en nuestros propios repositorios, que es donde una colección así debe empezar. Cinco de ellos, con el código, y el hábito que habría atrapado cada uno antes.
Espécimen uno: la suite que verificaba un fantasma
Migrando un sitio entre dos renderizadores, nuestra suite leía el directorio de salida publicado y verificaba cientos de propiedades contra él:
SWA = ROOT / "swa" # the directory the deploy uploads
def test_every_route_declares_its_canonical():
for page in SWA.rglob("index.html"):
assert '<link rel="canonical"' in page.read_text()Verde durante semanas. Cuatrocientas veinte rutas verificadas. El problema: el swa/ seguía escribiéndolo el renderizador antiguo. El nuevo, que era el objetivo entero del proyecto, escribía en .build/astro/, y ningún test abría esa carpeta. Estábamos verificando el artefacto que íbamos a sustituir.
La cura no fueron más aserciones. Fue una pregunta hecha a la suite entera, ¿qué bytes abre esto, realmente?, respondida haciendo que la suite componga primero el artefacto publicable y añadiendo un test cuyo único trabajo es fallar si el directorio publicado no es esa composición:
def test_the_published_artefact_is_the_composition():
# `data-page-owner` is written by the new renderer and nothing else.
# If no published page carries it, swa/ is still the old site.
owned = [p for p in SWA.rglob("*.html") if "data-page-owner" in p.read_text()]
assert owned, "the published artefact is the old renderer; the composition never shipped"Aburrido, estructural, y el test más valioso del repositorio, porque valida la única relación que todos los demás asumen en silencio.
Espécimen dos: la puerta que leía texto, no comportamiento
Nuestro recolector de evidencia lleva una promesa dura: solo lectura. Se impone parseando cada archivo PowerShell y recorriendo el árbol de sintaxis en busca de verbos mutantes:
def test_no_powershell_file_has_a_write_path():
for f in COLLECTOR.rglob("*.ps1"):
ast = parse_powershell(f.read_text())
verbs = [c.command_name for c in ast.find_all(CommandAst)]
assert not [v for v in verbs if v.split("-")[0] in MUTATING], fUna buena puerta. También una puerta sobre texto. Una versión nos enseñó la diferencia: un Import-Module -Force anidado descargaba un helper compartido de la sesión de quien llamaba, y el recolector no podía correr en absoluto, en ningún tenant y en ningún modo:
# The defect. -Force on a nested import re-imports the module fresh,
# evicting the already-loaded copy from the caller's session.
Import-Module (Join-Path $PSScriptRoot 'Evidence.psm1') -Force # Initialize-Evidence now gone upstreamTodas las puertas de texto siguieron verdes, porque el texto estaba bien. El programa que el texto describía estaba roto. La corrección fue una segunda guardia que carga los módulos exactamente como los carga el recolector y pregunta a la sesión viva qué sobrevivió:
Describe 'the modules load in the collector''s own order' {
It 'keeps Initialize-Evidence reachable after the siblings import' {
Import-Module ./modules/Evidence.psm1
Import-Module ./modules/Sharing.psm1 # must not evict the above
Get-Command Initialize-Evidence -ErrorAction Stop | Should -Not -BeNullOrEmpty
}
}La lección generaliza más allá de PowerShell: para cada propiedad que imponga estáticamente, pregunte qué pasa si el código pasa la comprobación y aun así no hace la cosa. Si nada lo notara, su puerta prueba analizabilidad, no comportamiento, y debería decirlo en voz alta en vez de tomar prestada la autoridad de la afirmación más fuerte.
Espécimen tres: la tabla publicada que nunca corría
Nuestro motor documenta cómo razona sobre evidencia incompleta: una tabla de operadores y cotas, qué lado puede decidir, qué lado no puede nunca:
operator lower bound proves what it cannot prove
> (min) pass fail (needs an upper bound)
not-contains fail on known part pass (needs the whole set)
== nothing decides only when single and fully observedEstaba en el documento de arquitectura, citada, pulida, y, como reveló un informe de cobertura, apenas la ejecutaba test alguno. Verdadera el día en que se escribió y sin guardia para siempre; cualquier refactor podía invertir una fila en silencio y el documento seguiría prometiendo el comportamiento antiguo con cara seria. Así que la tabla pasó a ejecutable, un caso por celda, generado de la misma fuente que el documento presenta:
@pytest.mark.parametrize("operator,bound,expected", CASES_FROM_THE_TABLE)
def test_each_cell_of_the_published_table(operator, bound, expected):
assert decide(operator, bound) is expected
def test_a_bound_of_none_decides_nothing():
assert decide(">", bound=None) is Outcome.UNKNOWNUna garantía publicada o corre en CI o es una esperanza con tipografía.
Espécimen cuatro: la fixture que halagaba al test
El más silencioso. Un test evalúa una regla y espera unknown; pasa:
def test_denied_sharing_run_is_all_unknown():
doc = load("coverage-denied-sharing.json")
assert doc.counts.unknown == 13 # ← where does 13 come from?La fixture se generó cuando un defecto de selección hacía que el motor ejecutara todas las reglas en vez de las dos del perfil. El 13 se copió de esa salida rota. No era una especificación; era un fósil de un defecto, mantenido vivo por un directorio de fixtures que nada refrescaba. Solo lo atrapamos cuando las fixtures se regeneraron desde el motor actual y el fósil discrepó de la realidad: el perfil selecciona dos reglas, así que el recuento honesto era tres, no trece.
El hábito que lo atrapa antes es un comentario:
# Three, because `--profile sharing` selects three rules since SPO-SHARE-005.
# It read thirteen for as long as a bare profile name failed open to every
# rule on disk, and this fixture was generated then.
assert doc.counts.unknown == 3Un número fijado que no se puede citar es un número copiado de lo que el sistema hizo el día en que se escribió el test, y el sistema aquel día podía estar equivocado.
Espécimen cinco: los tests que probaban las reglas y nunca el cableado
El mayor, y el más cercano a casa. Sesenta y cinco tests, todos en verde, cubrían una API regla a regla: validación, límites de ritmo, el techo de la moderación, la forma de un token. Ninguno abría el socket que abre una petición. Cada test ejercitaba un helper puro; ninguno conducía el handler HTTP que junta esos helpers en una respuesta. Las reglas estaban probadas. El cableado entre ellas se daba por supuesto.
Detrás de ese verde había un mecanismo que no podía funcionar en producción. Una edición de la newsletter registra quién ya la recibió, para que un reintento envíe solo a quien faltó. El marcador va a una tabla llamada newssent:
// The whole idempotency of a send rests on this row, and nothing creates the
// table it lives in.
await marker().upsertEntity(
{ partitionKey: editionId, rowKey: key(subscriber.email) },
"Replace",
);La producción no tenía tabla newssent. El primer envío mandaba la edición, fallaba al escribir el marcador en una tabla que no existía, y reportaba al destinatario como fallo. Peor, la guarda que lee el marcador trata una tabla ausente como "aún no enviado", así que cada reintento mandaba la lista entera otra vez. La idempotencia estaba muerta al nacer, y cada test seguía en verde, porque el doble en memoria que cada test usaba respondía al createTable con un encogimiento de hombros y al upsert con éxito. Un falso no puede no existir.
Lo que lo midió no fue otra aserción. Fue el mismo handler, conducido contra un Table Storage real corriendo localmente, con la fila persistida leída de vuelta:
// Real handler, real Table Storage, real OData. Only a table that can truly be
// absent exposes the missing create; only a real query proves the filter.
const res = await handler("newsletter-send")(admin(edition), ctx());
const written = await readAll(sent); // read the state back, do not trust the return
assert.equal(written.length, res.sent); // "sent" must imply "recorded"La corrección fue una línea que los otros almacenes ya tenían, un createTable defensivo antes del bucle. El defecto no es el punto. El punto es que una suite puede probar todas las reglas que un sistema tiene y nunca probar que el sistema corre, y la brecha queda invisible hasta que un test abre el mismo socket, y el mismo almacenamiento, que la producción abrirá.
Vale nombrar aquí una garantía, porque nombrarla es la mitad del trabajo. La entrega de un mensaje confirmado al buzón, y de una edición de la newsletter, es de entrega al menos una vez (at-least-once): un crash entre el envío y su registro repite el envío en el reintento, nunca lo deja caer. Es una elección. Un duplicado es una molestia; un mensaje perdido es un cliente. El test no afirma "exactamente una vez", que sería mentira; afirma que, tras un crash en cada punto, el trabajo sigue siendo recuperable, y nada se salta en silencio.
El verde es una afirmación, no un estado
Cada test que pasa afirma una relación entre tres cosas: el código, el artefacto bajo test, y el mundo que el artefacto va a encontrar. Cada espécimen rompió una pata distinta. La suite medía el artefacto equivocado. La puerta medía texto en vez de comportamiento. El documento prometía lo que nada ejecutaba. La fixture congeló un mundo roto y lo llamó esperado. La última suite probaba todas las reglas y nunca las corría juntas.
La cura es una pregunta con cinco disfraces, y merece la pena hacérsela hoy a su suite más verde: si esto dejara de ser verdad, ¿algo aquí se pondría rojo? Tome su panel más tranquilizador, su insignia de CI más estable, y siga una luz verde hasta los bytes que abre. Si el rastro termina en un artefacto que nadie publica, en una promesa que nadie corre o en un número sin fuente, la luz no está midiendo el sistema.
Lo está decorando.