Saltar al contenido
Development

Demostrar que un fallback realmente funciona como respaldo

Por Victor Da Luz
astrotestingimagesdev-logastro-tools

Mi generador de tarjetas OG tenía una sola prueba. Renderizaba una tarjeta de prueba (fixture) y verificaba que la salida fuera un PNG no vacío. Es una buena prueba de humo, y una vez detectó un fallo real, un pánico nativo en la librería de PNG que usaba antes de cambiar a sharp. Pero dejaba la superficie configurable real del paquete, dimensiones personalizadas y fuentes personalizadas, completamente sin verificar.

Ampliarla se convirtió en una pequeña lección sobre lo que realmente hace falta para “probar una opción”.

El 80% fácil

El ancho y alto personalizados fueron sencillos: generar una tarjeta de 600x400 en vez del valor por defecto de 1200x630, y luego verificar las dimensiones reales en píxeles del PNG resultante con sharp(png).metadata(). No “¿se renderizó?”, sino “¿el ancho es literalmente 600?”.

const png = await generateCard(markup, { width: 600, height: 400 });
const { width, height } = await sharp(png).metadata();
assert.equal(width, 600);
assert.equal(height, 400);

Volví y agregué la misma verificación de dimensiones a la prueba de humo original también. Esta verificaba “¿es un PNG válido de tamaño no trivial?”, cierto, pero seguiría siendo cierto aunque el ancho y el alto se ignoraran silenciosamente.

La parte que me hizo detenerme

La opción fonts debería permitir que quien consume el paquete reemplace por completo la fuente predeterminada incluida. Mi primer instinto para probarla: llamar a generateCard con una fuente personalizada y verificar que siguiera produciendo un PNG válido. Listo, ¿no?

Excepto que esa prueba pasaría exactamente igual sin importar si la opción hacía algo o no. Si fonts se ignorara en silencio y la función siempre volviera a su fuente predeterminada, la salida seguiría siendo un PNG válido, porque la fuente predeterminada funciona. Una prueba construida enteramente sobre “¿esto corre sin fallar?” no puede distinguir “la ruta de código se ejecutó” de “la ruta de código se saltó y otra cosa compensó”.

Una prueba que realmente demuestra algo necesita un caso donde los dos comportamientos diverjan. No tengo una segunda fuente real incluida para usar en una prueba positiva, pero sí tengo una forma de forzar la divergencia: pasar un arreglo de fuentes vacío.

await assert.rejects(() => generateCard(imperfectSystemsCard, { fonts: [] }));

Satori necesita una fuente para renderizar cualquier texto. Si fonts: [] llega hasta Satori, renderizar markup con mucho texto lanza un error. Si la opción se estuviera ignorando en silencio y se usara la fuente predeterminada en su lugar, nada lanzaría un error y la tarjeta se renderizaría sin problemas. Así que esta prueba solo puede pasar si la anulación realmente llega hasta la llamada de renderizado. Una prueba pequeña, casi al revés de lo esperado (afirma un fallo, no un éxito), pero es la que distingue entre “funciona” y “parece que funciona”.

La forma general del error

«Pasar una opción y confirmar que nada falló» prueba la resiliencia del camino feliz, no la opción. La señal de alerta es preguntar: si yo eliminara en silencio esta función y convirtiera la opción en un no-op, ¿esta prueba seguiría pasando? Si la respuesta es sí, no está probando la función. Para anulaciones y fallbacks en particular, la solución casi siempre es construir una entrada donde el fallback y la anulación difieran visiblemente, y luego afirmar cuál de los dos ocurrió realmente.

También en esta tanda

Las dimensiones de imagen de OgMeta estaban fijas en 1200x630 aunque quien consumiera el paquete pudiera pasar una imagen de otro tamaño; ahora son props opcionales con esos valores por defecto, más twitter:image:alt (antes solo existía la variante og:) y article:published_time/article:modified_time para las entradas de blog. Y una nota en el README que deja explícito que incluir sharp nativo incluso para quienes solo consumen las meta-etiquetas es una decisión deliberada y no un descuido. La generación de tarjetas es el objetivo del paquete, y un segundo paquete sin dependencias para el caso raro de solo meta-etiquetas no vale la pena mantenerlo.

Lecturas relacionadas