Escribir pruebas que verifican lo correcto
Hoy cerré un ítem chico del backlog: dos archivos con vacíos de cobertura reales de una auditoría anterior. Un job nunca había tenido pruebas, y un cliente HTTP tenía cero pruebas. No es un trabajo emocionante a primera vista, pero terminó siendo un buen recordatorio sobre la diferencia entre una prueba que pasa y una prueba que en verdad verifica algo.
Lo que construí
Para el cliente de GitHub necesitaba una forma de probar las llamadas HTTP sin golpear la API real. En vez de inventar algo nuevo, fui a ver cómo el código ya resolvía esto, y lo encontré en un cliente hermano para otro servicio. Recibe un objeto opcional que puede reemplazar la conexión HTTP real, con el objeto real como valor por defecto en producción. Copié la forma exacta: mismo nombre de parámetro, misma lógica de respaldo, mismo comentario. Cuando ya existe un patrón que funciona, seguirlo vale más que una alternativa ingeniosa.
Para el job seguí el patrón que ya usaban todas las demás pruebas de jobs en la suite: reemplazar temporalmente la clase de la que depende por una falsa, y devolver la real cuando termina la prueba. Directo, y coincidía con otros cuatro archivos que hacían lo mismo.
Lo que me sorprendió
Los dos conjuntos de pruebas pasaron limpios en la primera corrida. Eso debería haber sido tranquilizador. En cambio, cuando corrí una revisión sobre el diff, hizo una pregunta más útil que “esto pasa”, preguntó “qué atraparía en realidad esta prueba”.
Resultó que no mucho, en un par de puntos. El falso de la prueba del job no le importaba con qué argumentos lo llamaban, solo que lo llamaran. Si un cambio futuro dejaba caer en silencio una de las cuatro cosas que el job entrega (por ejemplo, olvidaba pasar el horario programado), todas las pruebas seguirían pasando, porque nada lo estaba verificando. Misma historia del lado del cliente HTTP: la conexión falsa ignoraba la petición real que se estaba construyendo y siempre devolvía una respuesta enlatada. Un bug que dañara la ruta de la URL, o que dejara caer el encabezado de autorización, pasaría sin ser detectado.
Los dos fueron rápidos de arreglar una vez identificados. Hice que los falsos capturaran con qué los llamaban, y agregué verificaciones sobre eso. Para el job: ¿le entrega el post correcto y los argumentos correctos a lo que en realidad habla con Postiz? Para el cliente: ¿construye una petición a la ruta correcta con el encabezado correcto? Cambios chicos, pero son la diferencia entre “esto no se cae” y “esto hace lo que se supone que debe hacer”.
La revisión también atrapó algo más mezquino y no negociable: había copiado un comentario de ese cliente hermano tal cual, con raya al medio incluida. La regla de estilo global dice que no se usan rayas al medio en ningún lado, ni siquiera en comentarios de código. Fácil de arreglar, pero un buen recordatorio de que copiar un patrón también significa copiar sus defectos si no se presta atención.
Última cosa: dos líneas de configuración que parecían necesarias no lo eran. Una configuraba una clave de API que el mocking de la prueba volvía irrelevante, la ruta de código real que la lee nunca corre cuando se reemplaza el objeto completo al que pertenece. No la borré solo por corazonada: la quité, volví a correr las pruebas, las vi seguir pasando, y solo entonces confié en que era seguro dejarla afuera.
Qué sigue
Nada pendiente en este. La lección que se repite en el trabajo de hoy: una suite de pruebas en verde dice que el código escrito no se cayó bajo las condiciones exactas para las que se escribió. Si en verdad atraparía una regresión real es una pregunta aparte, y vale la pena hacerla explícitamente en vez de asumir que la respuesta es sí.
Lecturas relacionadas
El shell del editor de posts, un bug de token vencido, y sin gema de mocking
Una tabla de borradores, autoguardado, y la primera llamada síncrona a GitHub en la app, más un bug que las pruebas en verde pasaron por alto y un helper de stub hecho a mano cuando minitest/mock no estaba disponible.
El bug de normalización que solo aparece con etiquetas hechas de nada
Un normalizador basado en strip se topa con una etiqueta de puro signo de puntuación: string vacío como clave de hash, sustitución de etiqueta equivocada, y un autocompletado que hace match con todo. Tres síntomas, una sola causa raíz.
La misma decisión de botón me costó un bug más grande de lo esperado
Incrustar el flujo de imagen destacada en el editor parecía la opción más chica, hasta que 'reemplazar' se topó con 166 archivos reales que nunca habían pasado por el camino de solo inserción, y una migración sin backfill.