Skip to content

Testing

Testing a Svelte app is the same pyramid as any frontend, with Svelte-aware helpers at the component layer:

LayerToolWhat it covers
UnitVitestPure logic, utilities, and runed .svelte.js modules
Component@testing-library/svelte + VitestA component rendered in jsdom — behaviour, not internals
End-to-endPlaywrightThe 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)"]
The testing pyramid for a Svelte app

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.

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

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.getByLabel('Email').fill('[email protected]');
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.

What is the guiding principle when testing components with Testing Library?
How do you test reactive state in a `.svelte.js` module?
Which tool verifies a full sign-in flow across real pages in a browser?