Mi pipeline llevaba meses en verde.

Corría en cada push, tenía sus 100 tests, subía el reporte cuando algo fallaba. Todo lo que uno pide de una CI decente.

Ejecutaba 44.

Los otros 56 se salteaban en silencio y el job terminaba igual, con su tilde verde, sin una línea que lo mencionara. Y no me enteré revisando el pipeline: me enteré cuando le apliqué a mi propia suite una rúbrica que había armado para auditar las de otros.

Dónde estás parada

Esto no es una parada de la serie Loop/Goal/Harness. Es la rúbrica completa con la que juzgo si una suite de tests sirve, publicada entera, y su primera aplicación real: mi propia suite. Si vienes de el goal verificable o de la métrica más mentirosa, acá esas ideas dejan de ser modelo mental y se vuelven un número que se puede defender.

Por qué me audité a mí misma antes que a nadie

Cuando armas un instrumento para evaluar el trabajo ajeno, tienes dos opciones. Estrenarlo con el trabajo de otro, o estrenarlo con el tuyo.

La primera es más cómoda y bastante más deshonesta. Un instrumento que nunca se aplicó a quien lo escribió no está calibrado: está declarado. Y hay una diferencia enorme entre creer que tu criterio es bueno y haberlo puesto a producir un número sobre tu propio código, delante de todos.

Así que le pasé la rúbrica a la suite de Academia sin Humo. La plataforma donde mis alumnos hacen los cursos. 100 tests, once archivos, 2.129 líneas escritas por mí.

Saqué 15 sobre 21. Aprueba, con deuda. Abajo está cada punto y de dónde salió.

La rúbrica: siete dimensiones, cero adjetivos

Antes de esto yo ya tenía tres checklists publicados. Diez preguntas sobre confiabilidad, seis sobre las piezas de un framework, cuatro sobre el oráculo. Todos útiles, y ninguno servía para lo que necesitaba.

Un checklist responde sí o no. Sirve para orientarte, no para dictaminar. Si tu suite tiene siete de diez, ¿aprueba? ¿Y si le faltan las tres más importantes?

Una rúbrica agrega lo que al checklist le falta: niveles. Dice qué es un 0, qué es un 1, qué es un 2 y qué es un 3 en cada dimensión. Con eso puedes poner un número, defenderlo con evidencia y compararlo dentro de seis meses.

Son siete, de 0 a 3, máximo 21:

  1. Oráculo — de dónde sale el valor esperado.
  2. Sensibilidad — si se pone roja por el motivo correcto.
  3. Determinismo — si un rojo significa que hay un bug.
  4. Diagnóstico — qué te dice cuando falla a las 3 a.m.
  5. Cobertura de reglas — cuáles protege y cuáles quedan fuera.
  6. Vida en CI — quién la corre y qué bloquea.
  7. Mantenibilidad — qué cuesta sostenerla el año que viene.

Las dos primeras son eliminatorias: si alguna puntúa 0, no aprueba aunque el resto sume. Una suite que congeló los bugs de hoy, o que no puede ponerse roja, no es una red de seguridad. Es un decorado caro que además da confianza, que es lo peor que puede hacer.

La rúbrica completa, con los niveles de cada dimensión

Las cuatro descripciones de nivel de las siete dimensiones, las bandas de veredicto y el recorrido corto para aplicarla en quince minutos están en la guía Cómo juzgar si una suite de tests sirve. Esa es la herramienta; esto de acá es lo que encontró cuando se la apliqué a mi propio trabajo.

Paso 0: la rúbrica reprobó a mi suite por hacer bien su trabajo

Acá viene la parte que no esperaba.

Apliqué la D1 y mi suite se fue a 0. Eliminatoria. Reprobada en la primera dimensión.

El motivo: 17 de mis 100 tests afirman bugs a propósito. Son el playground de mi curso de cazador de bugs, una app rota adrede donde los alumnos practican. Los tests fijan cada bug conocido, y verde significa “el bug sigue ahí”. Está documentado en el config y cada test cita el requisito que el bug viola:

BUG I-2: el cupo no se reduce al inscribirse (viola REQ-C04)

Según mi rúbrica, eso era un 0: valores esperados sacados de correr el sistema, con los bugs adentro. Según la realidad, era exactamente lo que esos tests debían hacer, hecho con más disciplina que la mayoría de las suites que veo.

El defecto era del instrumento. La D1 asumía que toda suite persigue el mismo objetivo, y no es así.

Una rúbrica que no sabe para qué existe la suite que está juzgando no está midiendo: está imponiendo su propio supuesto.

Así que la rúbrica ahora empieza en un paso anterior a cualquier puntaje: escribir en una línea qué protege esta suite. Una red de regresión protege el comportamiento correcto. Un arnés de bugs fijados afirma uno incorrecto a propósito. Una caracterización de legado congela el actual sin juzgarlo. Se puntúan distinto, y una suite mixta se puntúa por partes, nunca en promedio.

Eso dejó mi suite partida en dos: 17 tests que fijan bugs y 76 que protegen la academia real, la que usan mis alumnos. Los 15 sobre 21 son de esos 76.

Los siete puntajes, con evidencia

Nada de adjetivos. Cada nota con su motivo.

3 D1 Oráculo — nombres que citan la regla y el código esperado
2 D2 Sensibilidad — 314 asserts, ninguno verificado a propósito
2 D3 Determinismo — cero esperas fijas, pero retries: 2 en CI
3 D4 Diagnóstico — trace, reporte y nombres que dicen qué se rompió
2 D5 Cobertura — negativos y límites, sin declarar qué queda fuera
1 D6 Vida en CI — acá está toda la historia
2 D7 Mantenibilidad — 11 helpers duplicados en tres archivos

Las dos notas de 3 me las gané con los nombres de los tests, que resultaron ser lo mejor que tenía:

certificado sin completar nada → 409 requisitos_no_cumplidos
quiz idempotente: aprobar dos veces deja UNA sola entrada
quiz con answers de largo incorrecto → 422

Un nombre así te dice qué regla se rompió sin abrir el código. Eso es la D4 en su nivel máximo y no cuesta nada más que escribirlo pensando en quien va a leer el reporte a las tres de la mañana.

El 1 es otra historia.

El primer hallazgo: 56 de 100 tests no corrían

Mi suite tiene tests que necesitan base de datos. Están agrupados en bloques describe que se saltean cuando la variable de conexión no está.

En mi máquina, con la variable puesta, corrían. En CI la variable nunca se cargó, así que se salteaban los 56. Y saltear, en Playwright y en casi cualquier runner, no es fallar. El job terminaba con exit 0 y su tilde verde.

Lo peor es que estaba documentado. Lo había escrito yo misma en el workflow, meses atrás:

# Si el secret no está cargado, queda vacío: la migración se saltea
# y los tests "Con base de datos" se omiten (la CI sigue verde,
# comportamiento actual).

Lo sabía. Lo anoté. Y seguí adelante, porque el pipeline estaba verde y el verde tranquiliza.

44 / 100 tests que la CI ejecutaba de verdad
20 / 76 de los que protegen la academia real
0 avisos de que eso estaba pasando

Del subconjunto que protege la parte donde hay alumnos con cuenta, progreso y certificados, corría el 26%.

El verde es una afirmación sobre lo que corrió. No sobre lo que funciona. Y si nadie te dice qué corrió, el verde no afirma absolutamente nada.

El segundo hallazgo: dos tests llevaban semanas mintiendo

Cerré el hueco: dos jobs, uno con base y otro sin, para que se ejecuten los 100 tests entre ambos.

La primera vez que esos 56 tests corrieron en CI, dos fallaron.

S-C2: el catálogo lista playwright-cazador-bugs e ia-para-qa
S-C4: ia-para-qa sin botón de acción (en preparación)

Los dos exigían que el curso “IA aplicada al testing” no tuviera botón de inscripción, porque estaba en preparación.

Ese curso se publicó hace tiempo. Tengo alumnos que lo terminaron y tienen el certificado emitido.

O sea: mi suite llevaba semanas afirmando que un curso seguía sin lanzarse mientras había gente que ya lo había cursado entero. Y no fallaba, porque esos tests vivían justo en el grupo que la CI nunca ejecutaba.

La frase que quiero que te lleves de acá

Un test que nadie ejecuta no es un test que pasa. Es un test cuyo resultado nadie conoce. Y mientras tanto figura en tu conteo, en tu reporte y en la sensación de que estás cubierta.

La causa raíz no fue descuido

Esto es lo que más me sorprendió al ir a buscar el origen.

Los tests estaban mal porque copiaron un comentario que estaba mal. En la página del catálogo, la documentación decía:

REQ-C4: ia-para-qa no es navegable (badge "En preparación", sin botón de enroll).

Pero en el código la regla nunca fue sobre ese curso. Es genérica:

const hasContent = getCourseContent(course.id) !== undefined;
// ...
{!hasContent && <span>En preparación</span>}

Un curso sin contenido muestra el badge. Cualquier curso. El comentario había convertido una regla en una lista de casos, y los tests copiaron la lista.

Escribir el test contra el ejemplo: "ia-para-qa no tiene botón".
Escribir el test contra la regla: "un curso sin contenido no tiene botón".

La regla siguió siendo verdadera todo el tiempo. La lista se venció el día que cargué el contenido del curso, y no había nada que avisara, porque una lista no sabe que caducó.

Cómo lo cerré

Tres cambios, todos en el repo.

Dos jobs en vez de uno. Descubrí que ninguna configuración puede correr los 100 tests: hay dos que verifican que la API responda “base no disponible”, y esos solo pueden correr sin base. Con un solo job, algo siempre iba a quedar sin ejecutarse.

El silencio pasó a ser ruido. El job que necesita base lleva una variable que lo obliga a fallar si el secret no está, en vez de saltearse los 56 tests y terminar en verde. Saltear está bien. Saltear sin que nadie se entere, no.

Un candado contra producción. Los tests crean usuarios y la limpieza ejecuta un DELETE. Si la conexión apuntara por error a la base real, la suite escribiría ahí. Ahora un globalSetup se niega a arrancar si encuentra un solo usuario que no sea de test, sin necesidad de saber nada sobre producción. El accidente estaba a un copy-paste de distancia, y tener cuidado no es un control.

Resultado: 45 tests en un job, 99 en el otro, los 101 cubiertos entre ambos, los tres jobs en verde. La D6 pasó de 1 a 3 y la suite de 15 a 17.

Cómo le pasas esto a tu suite esta semana

No necesitas la rúbrica entera para el primer hallazgo. Necesitas una pregunta y quince minutos.

Abre la última corrida verde de tu CI y busca el conteo de skipped. No el de passed, que es el que todos miramos. El de skipped.

Si ese número no es cero, esos tests figuran en tu suite, cuentan en tu total, y no sabes cuál sería su resultado. Después pregúntate por qué se saltean, y si esa razón está escrita en algún lado o vive en la cabeza de alguien.

Ese es el corte que a mí me dolió. Los otros dos —de dónde salen tus valores esperados, y si un test se pone rojo cuando rompes a mano la regla que dice proteger— están en la guía de la rúbrica, con las siete dimensiones completas.

Lo que quiero que te lleves

Mis tests están bien escritos. Nombres que dicen qué regla se rompió, negativos, idempotencia, persistencia entre dispositivos, cero esperas fijas. Nada de eso impidió que durante meses la mitad de esa suite no se ejecutara y que dos tests afirmaran algo falso sobre mi propio producto.

La calidad de los tests y la calidad de la información que te dan son dos cosas distintas. La primera se ve leyendo el código. La segunda solo se ve preguntándole al pipeline qué corrió de verdad, y esa pregunta casi nadie la hace, porque el verde ya la respondió por nosotros.

Mi suite saca 17 sobre 21. La publiqué con el número, con la evidencia y con los dos hallazgos que me dejaron peor parada, porque un criterio que solo se estrena con el trabajo ajeno no está calibrado.

Ve a mirar tu conteo de skipped. Te va a llevar quince minutos y es probable que no te guste.