Estrategia de testing en Astro: qué merece prueba y qué no
Un test es una afirmación ejecutable con coste y valor: la estrategia no es escribir muchos, sino los que compran más confianza por unidad de esfuerzo. La pirámide y su economía, los tres ejes que encarecen cada nivel —velocidad, determinismo, acoplamiento—, las categorías que sí merecen prueba —lógica pura, endpoints y componentes con comportamiento—, lo que no la merece —markup trivial, configuración e internals— y el mapa de herramientas de Astro 7: Vitest para la lógica, la Container API para los componentes y Playwright para el navegador real.
La pirámide y su economía
La pirámide de tests ordena tus pruebas por granularidad y coste. En la base, ancha, viven los tests de unidad: rápidos, deterministas, aislados, tan baratos que puedes tener miles y ejecutarlos en segundos. En el centro, los de integración y componente: verifican que varias piezas colaboran, cuestan algo más y son algo más frágiles. En la cúspide, estrecha, los de extremo a extremo: arrancan un navegador real, recorren la aplicación como un usuario y compran la confianza más alta al precio más alto en tiempo y en fragilidad.
El antipatrón inverso —el cono de helado, ancho arriba y estrecho abajo— nace de una tentación comprensible: un test de extremo a extremo parece cubrir todo de una vez, así que se abusa de él. El coste llega después, cuando la suite tarda veinte minutos, falla de forma intermitente y nadie confía ya en su rojo. La disciplina de la pirámide es empujar cada afirmación al nivel más bajo donde aún sea significativa: si puedes probar una regla con una función pura, no montes un navegador para comprobarla.
Conviene nombrar los tres ejes que encarecen un test según sube de nivel. El primero es la velocidad: una función pura responde en microsegundos; un navegador tarda segundos en arrancar y pintar. El segundo es el determinismo: abajo, la misma entrada da siempre la misma salida; arriba, la red, el reloj y la concurrencia introducen intermitencia. El tercero es el acoplamiento: un test unitario depende de una función; uno de extremo a extremo depende de todo —servidor, navegador, estilos, datos—, así que rompe por más razones y diagnostica peor. Subir un peldaño multiplica los tres costes a la vez.
Nada de esto significa que los niveles altos sobren; significa que son caros y deben usarse con tino. Un test de extremo a extremo que protege el flujo de pago vale su precio; diez que reprueban detalles cosméticos son deuda que se paga en cada ejecución. La pirámide no prohíbe la cúspide: dosifica su uso y exige que cada test caro justifique por qué no podía vivir más abajo.
Qué merece una prueba
Tres familias concentran casi todo el valor. La primera es la lógica pura: utilidades, módulos de TypeScript, funciones que transforman datos, formatean fechas, calculan totales o validan una entrada con un esquema de Zod. Son entradas y salidas sin efectos, el terreno ideal del test unitario, y ofrecen el mayor retorno por línea escrita.
// src/lib/precio.tsexport function conIva(base: number, tipo = 0.21): number { if (base < 0) throw new RangeError('base negativa'); return Math.round(base * (1 + tipo) * 100) / 100;}La segunda familia son los endpoints y las actions: el contrato con el mundo exterior, donde se cruzan la validación de la entrada, los códigos de estado y las ramas de error. La tercera son los componentes con comportamiento: no el que solo interpola una prop, sino el que ramifica según sus props o compone una salida. Un ejemplo del tercer tipo aclara la frontera: este componente no es markup tonto, porque decide una clase según una prop, y esa decisión es lógica que puede romperse.
---interface Props { estado: 'activo' | 'pausado' }const { estado } = Astro.props;const clase = estado === 'activo' ? 'chip chip--ok' : 'chip chip--off';---<span class={clase}>{estado}</span>La afirmación valiosa aquí no es que exista un span —eso es trivial— sino que a la prop activo le corresponde la clase chip--ok y a pausado la contraria. Esa correspondencia es el comportamiento, y la Container API la prueba renderizando el componente con cada prop. De estos componentes se comprueba la conducta, no la apariencia píxel a píxel.
const html = await container.renderToString(Chip, { props: { estado: 'activo' },});expect(html).toContain('chip--ok');Dentro de “lo que merece prueba” aún hay prioridades: empieza por la lógica con más ramas y más reglas de negocio, porque es donde un descuido cuesta más caro y donde los tipos menos te protegen. Un validador con diez condiciones reclama tu atención antes que un formateador de una sola línea; ordenar el esfuerzo por riesgo, y no por comodidad, es parte de la estrategia.
Qué no merece una prueba
Simétrica a la anterior, esta lista te ahorra el mayor derroche. No merece prueba el markup trivial: un componente que solo vuelca una prop dentro de un encabezado no tiene comportamiento que romper. No merece prueba la configuración: tu astro.config y tus integraciones se verifican ejecutando el build, no con un test que reimplemente lo que hace Astro. Y no merecen prueba los internals del framework: su motor de render, su enrutado, su hidratación. Reprobarlos es duplicar la suite del proyecto y atarte a detalles que no controlas.
---const { titulo } = Astro.props;---<h1>{titulo}</h1>No hay decisión, no hay rama, no hay cálculo: solo se vuelca una prop en un encabezado. Los tipos garantizan que titulo existe y el compilador que el HTML es válido; un test aquí solo comprobaría que Astro sabe interpolar, que es reprobar el framework. La diferencia entre este componente y el chip del apartado anterior es exactamente la diferencia entre lo que merece prueba y lo que no: la presencia de comportamiento.
Cae también fuera de tu jurisdicción probar el código de terceros —una librería que ya trae su propia suite— y los artefactos generados, como los tipos que Astro sintetiza para tus colecciones o tus rutas. No son tuyos para garantizarlos, y reprobarlos solo añade ruido que se rompe cada vez que actualizas una dependencia o regeneras el proyecto. Prueba lo que escribes tú; confía en lo que ya viene probado.
El principio que unifica ambas listas es sencillo de enunciar y difícil de respetar bajo presión: prueba el comportamiento que un cambio tuyo podría romper, no el que solo rompería un bug del framework. Cada test que escribes es también un pasivo que habrá que mantener; el buen criterio no es maximizar la cobertura, sino maximizar la confianza que compra la suite descontado lo que cuesta arrastrarla.
El mapa de herramientas en Astro 7
Cada capa tiene su instrumento, y Astro 7 los integra sin fricción. Para la lógica pura y los módulos de TypeScript, vitest con el ayudante getViteConfig, que reutiliza tu configuración de Vite para que los tests vean los mismos alias, plugins e integraciones que el proyecto. Para los componentes .astro, la Container API —experimental_AstroContainer—, que renderiza un componente a una cadena de HTML en el mismo proceso, sin navegador, y te deja afirmar sobre la salida. Para los recorridos completos con navegador real, playwright, capaz de hidratar islas, seguir transiciones de vista y correr en integración continua.
import { getViteConfig } from 'astro/config'; // capa de unidadimport { experimental_AstroContainer } from 'astro/container'; // capa de componenteimport { test, expect } from '@playwright/test'; // capa de extremo a extremoCada uno de esos import es la puerta de entrada a un nivel de la pirámide, y reconocerlos de un vistazo te ahorra dudar dónde debe vivir cada prueba que escribes.
Observa que las tres herramientas no compiten: se reparten la pirámide. vitest puebla la base con cientos de afirmaciones veloces sobre tu lógica; la Container API cubre la franja media probando el render de servidor de tus componentes sin pagar el navegador; playwright corona la cúspide con el puñado de recorridos que solo existen cuando el cliente cobra vida. Elegir mal la herramienta —probar lógica con un navegador, o hidratación con la Container API— es el desperdicio más común, y los niveles que siguen enseñan, uno a uno, a poner cada afirmación en su sitio.