Skip to content

Testing & Tooling

An Astro site is mostly server-rendered HTML with a few islands, which shapes how you test it. Three layers cover it:

LayerToolWhat it checks
UnitVitestPlain logic — helpers, data transforms, schema parsing
ComponentAstro Container API (+ Vitest)A component renders the expected HTML for given props/slots
End-to-endPlaywrightReal pages in a real browser, including island interactivity

The rule of thumb: push logic into plain functions and unit-test those, render-test the components whose output matters, and use a few end-to-end tests for the flows that involve real hydration and navigation.

Because Astro runs on Vite, Vitest is the natural unit-test runner — same config, instant startup. Anything that’s plain TypeScript (a date formatter, a filter, a Zod schema) is tested the ordinary way:

import { expect, test } from 'vitest';
import { formatDate } from '../src/lib/format';
test('formatDate renders a friendly date', () => {
expect(formatDate(new Date('2026-01-15'))).toBe('Jan 15, 2026');
});

To test that a .astro component renders the right HTML, use the Astro Container API — it renders a component to a string in isolation, no browser needed. It’s currently exposed as experimental_AstroContainer:

import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { expect, test } from 'vitest';
import Card from '../src/components/Card.astro';
test('Card renders its title and slotted content', async () => {
const container = await AstroContainer.create();
const result = await container.renderToString(Card, {
props: { title: 'Hello' },
slots: { default: 'Body content' },
});
expect(result).toContain('Hello');
expect(result).toContain('Body content');
});

You get the rendered HTML as a string and assert against it — fast, and it exercises the real component including its props and slots.

Container tests render HTML but don’t run islands. For behavior that depends on hydration — clicking an interactive island, client-side navigation with <ClientRouter />, a form submission — use Playwright (or Cypress) to drive a real browser against your built site:

import { test, expect } from '@playwright/test';
test('counter island increments on click', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Increment' }).click();
await expect(page.getByText('Count: 1')).toBeVisible();
});
flowchart LR
  logic["plain logic"] --> vitest["Vitest unit test"]
  comp["component HTML output"] --> container["Container API test"]
  flow["hydrated flows and navigation"] --> pw["Playwright e2e"]
Test at the layer that matches the risk

In development, Astro shows a Dev Toolbar at the bottom of the page — it flags accessibility issues, shows which components are islands (and their hydration directives), and hosts plugins for auditing and debugging. It’s the fastest way to see what actually became an island and catch a11y problems while you build. It only runs in dev and never ships to production.

Which tool renders a .astro component to an HTML string in isolation for testing?
Why do you need Playwright in addition to Container API tests?
What is the natural unit-test runner for an Astro project, and why?
What does the Astro Dev Toolbar help with?