wandres.dev
TESTING · Vitest, Container, Playwright

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.

⏱ 14 min

No todo el código nace igual ante una prueba. Un test es una afirmación ejecutable sobre el comportamiento de tu sistema, y como toda afirmación tiene un coste —escribirla, mantenerla, ejecutarla en cada cambio— y un valor —la confianza que compra frente a una regresión—. La estrategia de testing no consiste en escribir muchos tests, sino en escribir los que devuelven más confianza por unidad de coste. En Astro, cuya arquitectura separa con nitidez lo que se hornea en el build de lo que atiende peticiones, esa economía cobra una forma peculiar: buena parte de la superficie es HTML estático que el compilador y los tipos ya validaron, de modo que el esfuerzo de prueba debe concentrarse allí donde de verdad vive la lógica que un cambio tuyo puede romper.

🎯 Al terminar esta lección sabrás
  • Comprender la pirámide de tests y los tres ejes que encarecen cada nivel.
  • Decidir qué merece prueba: la lógica pura, los endpoints y los componentes con comportamiento.
  • Reconocer qué no merece prueba: el markup trivial, la configuración y los internals del framework.
  • Asignar cada capa a su herramienta: vitest, la Container API y playwright.

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.

flowchart TD
E[e2e pocas lentas fragiles Playwright] --> C[componentes y endpoints algunas]
C --> U[unidad muchas rapidas deterministas Vitest]
style E fill:#f38ba8,color:#11111b
style C fill:#f9e2af,color:#11111b
style U fill:#a6e3a1,color:#11111b

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.ts
export 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.

💡
La pregunta que filtra: qué se rompe si me equivoco

Ante cualquier pieza, antes de escribir un test, hazte una pregunta operativa: si introduzco un fallo aquí, ¿lo atrapa el compilador, lo atrapan los tipos, o se escaparía hasta el usuario? Si los tipos ya lo atrapan —una prop mal tipada, una llamada con argumentos erróneos— el test es redundante. Si se escaparía silencioso —un cálculo torcido, una rama olvidada, un estado que no cambia— ahí, y solo ahí, un test compra confianza que nada más te da. La cobertura no distingue estos dos mundos; tu criterio sí.

🧪

Logica pura

Funciones y modulos TS: transformaciones, formateo, esquemas de validacion. Maximo retorno con vitest.

🔌

Endpoints y actions

El contrato externo: entrada, Response, codigos de estado y ramas de error.

🧩

Componentes con logica

Los que ramifican por props o calculan salida. Se prueba la conducta, no el markup.

🌐

Recorridos criticos

El puñado de flujos de usuario que no pueden romperse: prueba de extremo a extremo.

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 snapshot gigante es una trampa de baja señal

Capturar el HTML entero de una página en un snapshot parece cómodo: una línea y “queda cubierto”. Pero un snapshot enorme rompe ante cualquier cambio cosmético —una clase, un espacio, un atributo reordenado— sin distinguir la regresión real del retoque inocente. El resultado es un test que grita rojo a menudo y que el equipo aprende a actualizar a ciegas, es decir, a ignorar. Prefiere afirmaciones concretas —que exista este texto, que este enlace apunte aquí, que esta clase esté presente— sobre la fotografía indiscriminada del árbol completo.

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 APIexperimental_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 unidad
import { experimental_AstroContainer } from 'astro/container'; // capa de componente
import { test, expect } from '@playwright/test';               // capa de extremo a extremo

Cada 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.

flowchart LR
U[unidad y logica] --> VT[Vitest con getViteConfig]
MID[componentes astro] --> CA[Container API]
TOP[recorridos criticos] --> PW[Playwright y CI]
style VT fill:#a6e3a1,color:#11111b
style CA fill:#f9e2af,color:#11111b
style PW fill:#f38ba8,color:#11111b

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.

📝
Dónde viven los tests y cuándo corren

La convención más cómoda es co-locar: el test junto al código que prueba, precio.ts al lado de precio.test.ts, de modo que se muevan y se borren juntos. Durante el desarrollo, vitest en modo watch reejecuta solo lo afectado por cada cambio y te da el rojo o el verde al instante, convirtiendo la prueba en un diálogo continuo y no en un trámite final. Los tests de navegador, más lentos, se reservan para un comando aparte y para la integración continua. Cerca y rápido abajo; aparte y deliberado arriba.

La cobertura no es la meta; la confianza lo es

Existe una superstición tenaz en nuestra profesión: la de que un número alto de cobertura equivale a un sistema bien probado. La cobertura mide qué líneas ejecutó la suite, no qué afirmaciones hizo sobre ellas; puedes recorrer el cien por cien de tu código sin comprobar una sola salida, y puedes tener un cuarenta por ciento que cubra exactamente las reglas de negocio donde un error costaría dinero o reputación. La estrategia de testing madura invierte la pregunta: no “cómo subo la cobertura” sino “qué cosas, si se rompieran, me enterarían un usuario o un cliente antes que yo, y cuál es la prueba más barata que las atraparía primero”. Vista así, la pirámide deja de ser una regla estética y se revela como lo que es —una política de asignación de un recurso escaso, tu atención, hacia donde compra más certeza—. En Astro esa política tiene una ventaja estructural que conviene explotar sin complejos: como tanta parte de la aplicación es HTML estático que el compilador ya validó, puedes gastar menos en probar que la página existe y más en probar que la lógica que la alimenta es correcta. Un proyecto bien probado no es el que tiene más tests, sino aquel en el que cada rojo significa algo y cada verde se ha ganado la confianza que dice tener. El resto —los snapshots que nadie lee, los e2e que reprueban el framework, los tests de un getter— no son seguridad, son lastre disfrazado de diligencia, y su coste se paga en cada refactor que se pospone por miedo a tocar una suite que ya no distingue la señal del ruido.

⚔️ Dibuja tu propia pirámide
  1. Recorre un módulo de tu proyecto y clasifica cada pieza en una de las tres familias que sí merecen prueba: lógica pura, endpoint o componente con comportamiento.
  2. Señala al menos dos cosas que estabas tentado de probar y que caen en “no merece prueba”; escribe en una línea por qué el compilador o los tipos ya te cubren.
  3. Para una regla de negocio concreta, decide el nivel más bajo de la pirámide donde aún tendría sentido probarla y justifica por qué no subes un peldaño.
  4. Esboza el reparto de herramientas: qué probarás con vitest, qué con la Container API y qué reservas para playwright, y estima cuántos tests de cada clase esperas.