astro-blog finalmente tiene pruebas, y encontraron un error real
@vdaluz/astro-blog es uno de cuatro paquetes pequeños de componentes de Astro que se mantienen para estos sitios. Dos de los otros tres ya tenían suites de node --test. astro-blog tenía cero, lo cual empezaba a molestar cada vez que se tocaba la lógica de puntuación en RelatedPosts o el constructor de JSON-LD sin ninguna forma de saber si algo se había roto.
Así que llegó el momento de arreglar eso: cinco módulos de funciones puras, cinco archivos de prueba, siguiendo la misma convención que ya usaba el paquete hermano. node:test, node:assert/strict, importando directamente desde el código fuente de TypeScript sin ningún paso de compilación de por medio.
La pregunta de la devDependency
Uno de esos módulos importa astro/zod para construir un esquema de colección de contenido. Eso es una exportación de subruta del propio paquete astro, y este repositorio distribuye cero dependencias de runtime a propósito, es código fuente crudo, consumido por la versión de Astro que ya tenga instalada el sitio que lo llama. Correr un archivo de prueba que importa ese módulo fuera de una app real de Astro significaba que Node no podía resolver la importación en absoluto.
La solución fue agregar astro como devDependency, solo para pruebas. Nunca llega a quienes consumen el paquete, ya que npm no instala las devDependencies de un paquete cuando se depende de él. Pero valía la pena dejarlo explícito antes de tocarlo, porque “sin dependencias” es una restricción real de este repositorio, no solo una descripción de él.
La trampa del formato de fechas
El módulo de i18n tiene una función formatDate, y cualquier forma ingenua de escribir un dato de prueba para ella es una trampa. Construir la fecha de prueba a partir de una cadena ISO, interpretarla como medianoche UTC, y luego formatearla en la zona horaria en la que la prueba resulte estar corriendo, puede desviar el resultado un día entero según dónde esté ubicada la máquina respecto a UTC.
Las fechas de prueba se construyen a partir de componentes locales en lugar de cadenas ISO para el camino de configuración regional por defecto, y el camino del argumento de opciones se fija explícitamente a un timeZone específico para que no pueda desviarse. También se dejó de verificar una cadena exacta para la salida en configuración regional española, ya que el formato ICU para una etiqueta de idioma sin región ha cambiado antes entre versiones de Node. Esa prueba solo verifica las subcadenas que realmente importan.
Se corrió toda la suite bajo dos zonas horarias del sistema radicalmente distintas, UTC y UTC+14, antes de confiar en ella. Ambas limpias.
El error que las pruebas realmente atraparon
Uno de los módulos importa un módulo hermano sin extensión de archivo: from './relatedPosts' en vez de from './relatedPosts.ts'. Vite resuelve eso sin problema, así que funcionó en todos los sitios consumidores sin ningún inconveniente. La resolución de ESM de Node puro no. Lanza ERR_MODULE_NOT_FOUND, punto, sin alternativa.
Nadie había corrido este archivo fuera de un bundler antes. En el momento en que se hizo, que era todo el punto de agregar pruebas, se rompió de inmediato. Una corrección de una sola línea, pero es exactamente el tipo de cosa que “funciona en la app” tapa para siempre.
Lección
El error no estaba en la lógica que se estaba probando. Estaba en la plomería que permite que la lógica corra siquiera fuera de su bundler habitual. Ese es un buen argumento para probar código de librería con el corredor de pruebas más simple posible, en lugar de recurrir a la herramienta que la app consumidora use por casualidad. El corredor simple tiene menos opiniones propias, y menos opiniones significa menos cosas compensando en silencio por detrás.
Lecturas relacionadas
El error de escape que solo aparece con un segundo parámetro de consulta
Una función de reescritura que funcionaba en cada prueba y cada URL real que había visto, y aun así se habría roto en el momento en que alguien agregara un segundo parámetro de consulta.
Probar la parte del código que documenta su propia trampa
Una advertencia en el README sobre un modo de fallo sutil es una confesión de que el código todavía no tiene pruebas para eso.
La decodificación de 200KB que nadie necesitaba repetir
Un fix de memoización de una línea, y la pequeña prueba que realmente demuestra que la memoización ocurrió, en vez de solo confiar en el diff.