Svelte Testing Patterns
Test Svelte 5 components with Vitest and @testing-library/svelte — render, user events, store mocking, and async tick flushing
When to Use
- You are writing unit tests for Svelte components
- You need to test reactive behavior triggered by user interactions (clicks, input, form submission)
- You need to mock Svelte stores or module imports in tests
- You are testing async behavior and need to flush the update queue with
tick()
Instructions
Setup:
- Install dependencies and configure Vitest with the Svelte plugin:
npm install -D vitest @testing-library/svelte @testing-library/jest-dom jsdom
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte({ hot: !process.env.VITEST })],
test: {
environment: 'jsdom',
setupFiles: ['./src/tests/setup.ts'],
globals: true,
},
});
// src/tests/setup.ts
import '@testing-library/jest-dom';
Rendering components:
- Use
renderfrom@testing-library/svelteto mount components. Query the DOM with Testing Library queries:
import { render, screen } from '@testing-library/svelte';
import MyButton from '$lib/components/MyButton.svelte';
test('renders with label', () => {
render(MyButton, { props: { label: 'Click me' } });
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
- Pass props using the
propsoption:
render(UserCard, {
props: {
user: { id: 1, name: 'Alice', email: 'alice@example.com' },
},
});
Firing events:
- Use
fireEventor theuserEventlibrary for simulating interactions:
import { render, screen, fireEvent } from '@testing-library/svelte';
import Counter from '$lib/components/Counter.svelte';
test('increments on click', async () => {
render(Counter);
const button = screen.getByRole('button', { name: '+' });
await fireEvent.click(button);
await fireEvent.click(button);
expect(screen.getByText('2')).toBeInTheDocument();
});
- Use
@testing-library/user-eventfor more realistic interactions (typing, tabbing):
import userEvent from '@testing-library/user-event';
test('types in search input', async () => {
const user = userEvent.setup();
render(SearchBar);
await user.type(screen.getByRole('searchbox'), 'svelte');
expect(screen.getByDisplayValue('svelte')).toBeInTheDocument();
});
Async updates with tick:
- After triggering reactive updates, await
tick()fromsvelteto flush Svelte's update queue:
import { tick } from 'svelte';
test('shows error after failed submit', async () => {
render(LoginForm);
await fireEvent.submit(screen.getByRole('form'));
await tick();
expect(screen.getByText('Email is required')).toBeInTheDocument();
});
Testing stores:
- Mock writable stores by importing and setting them directly:
import { count } from '$lib/stores/counter';
import CounterDisplay from '$lib/components/CounterDisplay.svelte';
test('displays current count', () => {
count.set(42);
render(CounterDisplay);
expect(screen.getByText('42')).toBeInTheDocument();
});
Mocking modules:
- Use
vi.mockto mock module imports in tests:
vi.mock('$lib/services/api', () => ({
fetchUsers: vi.fn().mockResolvedValue([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
]),
}));
test('renders fetched users', async () => {
render(UserList);
await tick();
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('Bob')).toBeInTheDocument();
});
Testing with context:
- Wrap components in a context provider using a wrapper component, or use the
contextoption:
render(MyComponent, {
context: new Map([['theme', { primary: '#3b82f6' }]]),
});
Details
Svelte 5 vs. Svelte 4 testing differences:
Svelte 5 with @testing-library/svelte v5+ uses the new component API internally. The test API (render, screen, fireEvent) is unchanged from the user's perspective. Ensure @testing-library/svelte v5 or later for Svelte 5 components.
The Testing Library philosophy:
Prefer queries that reflect how users perceive the UI:
getByRole— most semantic (button, heading, checkbox, etc.)getByLabelText— for form fieldsgetByPlaceholderText— fallback for inputsgetByText— visible text contentgetByTestId— last resort (adddata-testidto elements)
Avoid testing implementation details (component state, store values) — test observable behavior.
act in Svelte:
Unlike React Testing Library, Svelte Testing Library uses tick() for flushing updates rather than act(). Most interactions already resolve automatically with await; tick() is needed when updates are triggered by direct reactive changes rather than DOM events.
Testing SvelteKit load functions:
Load functions are plain async functions — test them directly without mounting a component:
import { load } from './+page.server';
test('returns user data', async () => {
const result = await load({
params: { id: '1' },
locals: { user: mockUser },
fetch: global.fetch,
} as any);
expect(result.user.id).toBe(1);
});
Snapshot testing:
test('matches snapshot', () => {
const { container } = render(MyComponent);
expect(container).toMatchSnapshot();
});
Use sparingly — prefer behavior assertions over structural snapshots.
Source
https://kit.svelte.dev/docs/testing
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.