Atlasingeniería

Code review en la prácticaTests que pasan sin probar nadaTema 1

El test que pasa sin probar nada

Un mock sin configurar devolvía null, el código tiraba una excepción, un catch la escondía y el test seguía en verde. Cuatro formas reales en que una suite verde deja de significar algo, y cómo detectarlas.

Un test en verde dice una de dos cosas: que el código hace lo que se espera, o que el test no llegó a mirar. Las dos se ven exactamente igual en el pipeline.

Este post junta cuatro casos reales —encontrados en revisiones de código, sobre tests míos y de otras personas— en los que la suite estaba verde y no validaba lo que su nombre prometía.

El caso: la excepción que el catch se comía

El componente cargaba varias colecciones de reglas desde una base y las dejaba en memoria. Un cambio agregó cuatro colecciones nuevas. El test de esa carga usaba un doble de prueba para la base y configuraba la respuesta de tres colecciones de las cuatro.

La cadena completa

  1. Para la colección sin configurar, el doble devuelve null en vez de una lista vacía: es el comportamiento por defecto de un mock cuando nadie le dijo qué contestar.
  2. El código de carga hace .stream() sobre ese resultado y tira una excepción de puntero nulo.
  3. La carga de cada colección está envuelta en un try/catch —razonable: no querés que una colección rota tumbe el arranque de la aplicación—.
  4. El catch registra el error y sigue.
  5. El test pasa. Nadie afirma nada sobre esa colección, así que no hay assert que falle.

El resultado: un test que corría con una excepción silenciada en cada ejecución y que no validaba el mapeo de una de las cuatro colecciones nuevas —justamente la parte nueva del cambio—. Y el mismo patrón se repetía en los otros dos tests de la clase.

Código bajo prueba

const loadRules = (source) => {
const loaded = {};
for (const name of COLLECTIONS) {
  try {
    loaded[name] = source.fetch(name).stream().toList();
  } catch (error) {
    log.warn('no se pudo cargar', name, error);
  }
}
return loaded;
};

Metele un bug

El código está como lo escribió quien lo programó: toda la suite pasa.

La suite

  • pasacarga sin lanzar excepciónno tira
  • pasamapea las tres colecciones configuradasloaded.a, b y c
  • pasamapea las cuatro coleccionesObject.keys(loaded).length === 4

Probá las cuatro mutaciones. La prueba que nunca se pone en rojo es la que hay que reescribir, no la que hay que borrar: su nombre promete algo que nadie está verificando.

Las otras tres formas de estar en verde sin probar

SíntomaQué está pasandoCómo se detecta
Un doble sin configurar devuelve nullLa rama no se ejecuta o se rompe en silencioEjecutar el test y leer los logs, no sólo el resultado
El test pasa por el motivo equivocadoEl stub hace que la condición del nombre ni se evalúeRomper el código a propósito: si sigue verde, no probaba eso
El test usa la fecha de hoyPasa hoy y falla el 31 de un mes, o en otra zona horariaInyectar el reloj; correr la suite con otra fecha
Todo el flujo en un solo casoCuando falla, no se sabe en qué pasoSeparar en casos con nombre por comportamiento
Cuatro síntomas distintos, un mismo resultado: el pipeline en verde deja de ser información.

El segundo merece un párrafo. En un test llamado «no se muestra si ya se mostró hace poco», el doble que decidía esa condición estaba configurado para devolver siempre falso. El test pasaba —y hubiera pasado igual si alguien borraba entera la lógica de «hace poco»—. El nombre decía una cosa y la ejecución probaba otra.

Antes de seguir, predecí

¿Cuál es la forma más barata de saber si un test valida lo que dice su nombre?

Lo que sí funciona

Cinco hábitos que sostienen una suite honesta

  1. Escribir el test en rojo primero, aunque sea unos segundos. Un test que nunca estuvo en rojo no demostró que detecta nada.
  2. Mirar la salida de la corrida, no sólo el resultado. Un ERROR en los logs de una suite verde es un test ciego esperando a que alguien lo lea.
  3. Configurar todos los dobles que el código va a consultar, y que devuelvan colecciones vacías en vez de null: si el código sabe manejar una lista vacía, el test tiene que ejercitar ese camino, no el del puntero nulo.
  4. Inyectar el reloj. Si la producción llama a la fecha actual y el test también, no están comparando lo mismo: están comparando dos lecturas distintas del mismo reloj, y el día que caigan a los lados de la medianoche la suite se pone roja sin que nadie haya tocado el código.
  5. Un caso por comportamiento, con nombre. Cuando el pipeline falla a las tres de la mañana, el nombre del caso es el primer dato del diagnóstico.
Más a fondo · nivel seniorPor qué la cobertura no lo detecta

La cobertura de líneas registra ejecución, no verificación. En el caso del principio, la línea del .stream() se ejecuta —tira una excepción, pero se ejecuta—, el catch se ejecuta y ambas cuentan como cubiertas. Una suite con 90 % de cobertura puede tener cero afirmaciones útiles. Las métricas que sí correlacionan con detección son las que rompen el código a propósito y miden cuántos tests se ponen en rojo.

Lo que preguntan sobre esto

Cierre

Autoevaluación

¿Lo entendiste?

Un doble de prueba sin configurar devuelve null y el código tira una excepción atrapada por un catch. ¿Qué pasa con el test?
¿Qué demuestra un 90 % de cobertura de líneas?
¿Por qué conviene inyectar el reloj en vez de usar la fecha actual?