Testing with Vitest & Testing Library
React developers typically reach for Jest with React Testing Library (RTL) for component tests. Svelte’s equivalent is Vitest + @testing-library/svelte. The Testing Library philosophy is identical — query the DOM the way a user would, fire events, assert on visible output. The APIs are almost byte-for-byte the same. The main differences are the test runner (Vitest vs Jest) and the way components are imported (.svelte files vs .tsx).
Counter component test comparison
Section titled “Counter component test comparison”First, the components under test:
// Counter.tsximport React from 'react';
export function Counter() { const [count, setCount] = React.useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(c => c + 1)}>Increment</button> </div> );}<!-- Counter.svelte --><script lang="ts"> let count = $state(0);</script><div> <p>Count: {count}</p> <button onclick={() => count++}>Increment</button></div>Now the tests:
// Counter.test.tsx (Jest + RTL)import { render, screen, fireEvent } from '@testing-library/react';import { Counter } from './Counter';
test('increments count', () => { render(<Counter />); expect(screen.getByText('Count: 0')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Increment' })); expect(screen.getByText('Count: 1')).toBeInTheDocument();});// Counter.test.ts (Vitest + @testing-library/svelte)import { render, screen, fireEvent } from '@testing-library/svelte';import Counter from './Counter.svelte';import { describe, it, expect } from 'vitest';
describe('Counter', () => { it('increments count', async () => { render(Counter); expect(screen.getByText('Count: 0')).toBeInTheDocument(); await fireEvent.click( screen.getByRole('button', { name: 'Increment' }) ); expect(screen.getByText('Count: 1')).toBeInTheDocument(); });});Key differences to notice:
- Svelte:
render(Counter)— pass the component class directly (no JSX). React:render(<Counter />)— JSX element. - Svelte:
fireEvent.click(...)must beawaited because Svelte batches DOM updates asynchronously. React RTL’sfireEventis synchronous. - Everything else —
screen,getByText,getByRole,toBeInTheDocument— is identical.
Vitest configuration
Section titled “Vitest configuration”import { defineConfig } from 'vitest/config';import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({ plugins: [svelte({ hot: !process.env.VITEST })], test: { environment: 'jsdom', globals: true, setupFiles: ['./src/setupTests.ts'], },});The hot: !process.env.VITEST guard disables HMR during test runs (which would cause errors in the jsdom environment).
Installation
Section titled “Installation”npm install --save-dev vitest @testing-library/svelte @testing-library/jest-dom jsdomCreate src/setupTests.ts:
import '@testing-library/jest-dom';End-to-end testing with Playwright
Section titled “End-to-end testing with Playwright”Playwright works identically for both React and Svelte projects — there is nothing Svelte-specific to learn. Install and configure it the same way:
npm init playwright@latest