Testing
Three layers, three tools
Section titled “Three layers, three tools”Testing a Svelte app is the same pyramid as any frontend, with Svelte-aware helpers at the component layer:
| Layer | Tool | What it covers |
|---|---|---|
| Unit | Vitest | Pure logic, utilities, and runed .svelte.js modules |
| Component | @testing-library/svelte + Vitest | A component rendered in jsdom — behaviour, not internals |
| End-to-end | Playwright | The real app in a real browser, across pages |
flowchart TB e2e["Playwright: end-to-end (few)"] --> comp["Testing Library: components (some)"] comp --> unit["Vitest: logic + runed modules (many)"]
Unit tests with Vitest
Section titled “Unit tests with Vitest”Plain logic — formatters, calculations, a store’s update function — is tested with Vitest, which shares Vite’s config so it understands your project out of the box:
import { describe, it, expect } from 'vitest';import { formatPrice } from './money';
describe('formatPrice', () => { it('adds a currency symbol', () => { expect(formatPrice(5)).toBe('$5.00'); });});Reactive state in a .svelte.js / .svelte.ts module (runes outside components) is tested the same way — Vitest runs with the Svelte plugin so runes work in the test. Wrap assertions that depend on effects in $effect.root or flush with await tick() when needed.
Component tests with Testing Library
Section titled “Component tests with Testing Library”@testing-library/svelte renders a component into jsdom and gives you queries and interactions that mirror how a user behaves — query by role/text, not by internal class names:
import { render, screen } from '@testing-library/svelte';import userEvent from '@testing-library/user-event';import { expect, test } from 'vitest';import Counter from './Counter.svelte';
test('increments on click', async () => { render(Counter, { props: { start: 0 } }); const button = screen.getByRole('button', { name: /count/i });
await userEvent.click(button);
expect(button).toHaveTextContent('count: 1');});The discipline: test behaviour, not implementation. Assert on what the user sees and does (visible text, roles, interactions), so a refactor that keeps behaviour identical doesn’t break the test.
End-to-end with Playwright
Section titled “End-to-end with Playwright”Playwright drives a real browser through real pages — routing, form submission, navigation — which is exactly where SvelteKit’s server behaviour (load functions, form actions) needs verifying:
import { test, expect } from '@playwright/test';
test('user can sign in', async ({ page }) => { await page.goto('/login'); await page.getByRole('button', { name: 'Sign in' }).click(); await expect(page).toHaveURL('/dashboard');});Keep these few and high-value — they’re slower than unit and component tests, but they’re the only layer that proves the whole thing works together.