Skip to main content

Testing

Testing helps you write and maintain your code and guard against regressions. Testing frameworks help you with that, allowing you to describe assertions or expectations about how your code should behave. Svelte is unopinionated about which testing framework you use — you can write unit tests, integration tests, and end-to-end tests using solutions like Vitest, Jasmine, Cypress and Playwright.

Unit and integration testing using Vitest

Unit tests allow you to test small isolated parts of your code. Integration tests allow you to test parts of your application to see if they work together. If you’re using Vite (including via SvelteKit), we recommend using Vitest. You can use the Svelte CLI to setup Vitest either during project creation or later on.

To setup Vitest manually, first install it:

npm install -D vitest

Then adjust your vite.config.js:

vite.config
import { function defineConfig(config: UserConfig): UserConfig (+3 overloads)defineConfig } from 'vitest/config';

export default function defineConfig(config: UserConfig): UserConfig (+3 overloads)defineConfig({
	// ...
	// Tell Vitest to use the `browser` entry points in `package.json` files, even though it's running in Node
	
UserConfig.resolve?: (ResolveOptions & {
    alias?: AliasOptions;
}) | undefined
resolve
: var process: NodeJS.Processprocess.NodeJS.Process.env: NodeJS.ProcessEnvenv.string | undefinedVITEST
? { ResolveOptions.conditions?: string[] | undefinedconditions: ['browser'] } : var undefinedundefined });

If loading the browser version of all your packages is undesirable, because (for example) you also test backend libraries, you may need to resort to an alias configuration

You can now write unit tests for code inside your .js/.ts files:

multiplier.svelte.test
import { function flushSync<T = void>(fn?: (() => T) | undefined): TflushSync } from 'svelte';
import { const expect: ExpectStaticexpect, const test: TestAPItest } from 'vitest';
import { import multipliermultiplier } from './multiplier.svelte.js';

test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number | TestOptions): void (+2 overloads)test('Multiplier', () => {
	let let double: anydouble = import multipliermultiplier(0, 2);

	expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)expect(let double: anydouble.value).JestAssertion<any>.toEqual: <number>(expected: number) => voidtoEqual(0);

	let double: anydouble.set(5);

	expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)expect(let double: anydouble.value).JestAssertion<any>.toEqual: <number>(expected: number) => voidtoEqual(10);
});
multiplier.svelte
/**
 * @param {number} initial
 * @param {number} k
 */
export function 
function multiplier(initial: number, k: number): {
    readonly value: number;
    set: (c: number) => void;
}
multiplier
(initial: numberinitial, k: numberk) {
let let count: numbercount =
function $state<number>(initial: number): number (+1 overload)
namespace $state
$state
(initial: numberinitial);
return { get value: numbervalue() { return let count: numbercount * k: numberk; }, /** @param {number} c */ set: (c: number) => voidset: (c: numberc) => { let count: numbercount = c: numberc; } }; }
export function 
function multiplier(initial: number, k: number): {
    readonly value: number;
    set: (c: number) => void;
}
multiplier
(initial: numberinitial: number, k: numberk: number) {
let let count: numbercount =
function $state<number>(initial: number): number (+1 overload)
namespace $state
$state
(initial: numberinitial);
return { get value: numbervalue() { return let count: numbercount * k: numberk; }, set: (c: number) => voidset: (c: numberc: number) => { let count: numbercount = c: numberc; } }; }

Using runes inside your test files

Since Vitest processes your test files the same way as your source files, you can use runes inside your tests as long as the filename includes .svelte:

multiplier.svelte.test
import { function flushSync<T = void>(fn?: (() => T) | undefined): TflushSync } from 'svelte';
import { const expect: ExpectStaticexpect, const test: TestAPItest } from 'vitest';
import { import multipliermultiplier } from './multiplier.svelte.js';

test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number | TestOptions): void (+2 overloads)test('Multiplier', () => {
	let let count: numbercount = 
function $state<0>(initial: 0): 0 (+1 overload)
namespace $state
$state
(0);
let let double: anydouble = import multipliermultiplier(() => let count: numbercount, 2); expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)expect(let double: anydouble.value).JestAssertion<any>.toEqual: <number>(expected: number) => voidtoEqual(0); let count: numbercount = 5; expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)expect(let double: anydouble.value).JestAssertion<any>.toEqual: <number>(expected: number) => voidtoEqual(10); });
multiplier.svelte
/**
 * @param {() => number} getCount
 * @param {number} k
 */
export function 
function multiplier(getCount: () => number, k: number): {
    readonly value: number;
}
multiplier
(getCount: () => numbergetCount, k: numberk) {
return { get value: numbervalue() { return getCount: () => numbergetCount() * k: numberk; } }; }
export function 
function multiplier(getCount: () => number, k: number): {
    readonly value: number;
}
multiplier
(getCount: () => numbergetCount: () => number, k: numberk: number) {
return { get value: numbervalue() { return getCount: () => numbergetCount() * k: numberk; } }; }

If the code being tested uses effects, you need to wrap the test inside $effect.root:

logger.svelte.test
import { function flushSync<T = void>(fn?: (() => T) | undefined): TflushSync } from 'svelte';
import { const expect: ExpectStaticexpect, const test: TestAPItest } from 'vitest';
import { import loggerlogger } from './logger.svelte.js';

test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number | TestOptions): void (+2 overloads)test('Effect', () => {
	const const cleanup: () => voidcleanup = 
namespace $effect
function $effect(fn: () => void | (() => void)): void
$effect
.function $effect.root(fn: () => void | (() => void)): () => voidroot(() => {
let let count: numbercount =
function $state<0>(initial: 0): 0 (+1 overload)
namespace $state
$state
(0);
// logger uses an $effect to log updates of its input let let log: anylog = import loggerlogger(() => let count: numbercount); // effects normally run after a microtask, // use flushSync to execute all pending effects synchronously flushSync<void>(fn?: (() => void) | undefined): voidflushSync(); expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)expect(let log: anylog).JestAssertion<any>.toEqual: <number[]>(expected: number[]) => voidtoEqual([0]); let count: numbercount = 1; flushSync<void>(fn?: (() => void) | undefined): voidflushSync(); expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)expect(let log: anylog).JestAssertion<any>.toEqual: <number[]>(expected: number[]) => voidtoEqual([0, 1]); }); const cleanup: () => voidcleanup(); });
logger.svelte
/**
 * @param {() => any} getValue
 */
export function function logger(getValue: () => any): any[]logger(getValue: () => anygetValue) {
	/** @type {any[]} */
	let let log: any[]log = [];

	
function $effect(fn: () => void | (() => void)): void
namespace $effect
$effect
(() => {
let log: any[]log.Array<any>.push(...items: any[]): numberpush(getValue: () => anygetValue()); }); return let log: any[]log; }
export function function logger(getValue: () => any): any[]logger(getValue: () => anygetValue: () => any) {
	let let log: any[]log: any[] = [];

	
function $effect(fn: () => void | (() => void)): void
namespace $effect
$effect
(() => {
let log: any[]log.Array<any>.push(...items: any[]): numberpush(getValue: () => anygetValue()); }); return let log: any[]log; }

Component testing

It is possible to test your components in isolation using Vitest.

Before writing component tests, think about whether you actually need to test the component, or if it’s more about the logic inside the component. If so, consider extracting out that logic to test it in isolation, without the overhead of a component

To get started, install jsdom (a library that shims DOM APIs):

npm install -D jsdom

Then adjust your vite.config.js:

vite.config
import { function defineConfig(config: UserConfig): UserConfig (+3 overloads)defineConfig } from 'vitest/config';

export default function defineConfig(config: UserConfig): UserConfig (+3 overloads)defineConfig({
	UserConfig.plugins?: PluginOption[] | undefinedplugins: [
		/* ... */
	],
	UserConfig.test?: InlineConfig | undefinedtest: {
		// If you are testing components client-side, you need to setup a DOM environment.
		// If not all your files should have this environment, you can use a
		// `// @vitest-environment jsdom` comment at the top of the test files instead.
		InlineConfig.environment?: VitestEnvironment | undefinedenvironment: 'jsdom'
	},
	// Tell Vitest to use the `browser` entry points in `package.json` files, even though it's running in Node
	
UserConfig.resolve?: (ResolveOptions & {
    alias?: AliasOptions;
}) | undefined
resolve
: var process: NodeJS.Processprocess.NodeJS.Process.env: NodeJS.ProcessEnvenv.string | undefinedVITEST
? { ResolveOptions.conditions?: string[] | undefinedconditions: ['browser'] } : var undefinedundefined });

After that, you can create a test file in which you import the component to test, interact with it programmatically and write expectations about the results:

component.test
import { function flushSync<T = void>(fn?: (() => T) | undefined): TflushSync, function mount<Props extends Record<string, any>, Exports extends Record<string, any>>(component: ComponentType<SvelteComponent<Props>> | Component<Props, Exports, any>, options: MountOptions<Props>): Exportsmount, 
function unmount(component: Record<string, any>, options?: {
    outro?: boolean;
} | undefined): Promise<void>
unmount
} from 'svelte';
import { const expect: ExpectStaticexpect, const test: TestAPItest } from 'vitest'; import
type Component = SvelteComponent<Record<string, any>, any, any>
const Component: LegacyComponentType
Component
from './Component.svelte';
test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number | TestOptions): void (+2 overloads)test('Component', () => { // Instantiate the component using Svelte's `mount` API const
const component: {
    $on?(type: string, callback: (e: any) => void): () => void;
    $set?(props: Partial<Record<string, any>>): void;
} & Record<string, any>
component
=
mount<Record<string, any>, {
    $on?(type: string, callback: (e: any) => void): () => void;
    $set?(props: Partial<Record<string, any>>): void;
} & Record<...>>(component: ComponentType<...> | Component<...>, options: MountOptions<...>): {
    $on?(type: string, callback: (e: any) => void): () => void;
    $set?(props: Partial<Record<string, any>>): void;
} & Record<...>
mount
(const Component: LegacyComponentTypeComponent, {
target: Document | Element | ShadowRoottarget: var document: Documentdocument.Document.body: HTMLElementbody, // `document` exists because of jsdom props?: Record<string, any> | undefinedprops: { initial: numberinitial: 0 } }); expect<string>(actual: string, message?: string): Assertion<string> (+1 overload)expect(var document: Documentdocument.Document.body: HTMLElementbody.Element.innerHTML: stringinnerHTML).JestAssertion<string>.toBe: <string>(expected: string) => voidtoBe('<button>0</button>'); // Click the button, then flush the changes so you can synchronously write expectations var document: Documentdocument.Document.body: HTMLElementbody.ParentNode.querySelector<"button">(selectors: "button"): HTMLButtonElement | null (+4 overloads)querySelector('button').HTMLElement.click(): voidclick(); flushSync<void>(fn?: (() => void) | undefined): voidflushSync(); expect<string>(actual: string, message?: string): Assertion<string> (+1 overload)expect(var document: Documentdocument.Document.body: HTMLElementbody.Element.innerHTML: stringinnerHTML).JestAssertion<string>.toBe: <string>(expected: string) => voidtoBe('<button>1</button>'); // Remove the component from the DOM
function unmount(component: Record<string, any>, options?: {
    outro?: boolean;
} | undefined): Promise<void>
unmount
(
const component: {
    $on?(type: string, callback: (e: any) => void): () => void;
    $set?(props: Partial<Record<string, any>>): void;
} & Record<string, any>
component
);
});

While the process is very straightforward, it is also low level and somewhat brittle, as the precise structure of your component may change frequently. Tools like @testing-library/svelte can help streamline your tests. The above test could be rewritten like this:

component.test
import { function render<C extends unknown, Q extends Queries = typeof import("/vercel/path0/node_modules/.pnpm/@[email protected]/node_modules/@testing-library/dom/types/queries")>(Component: ComponentType<...>, options?: SvelteComponentOptions<C>, renderOptions?: RenderOptions<Q>): RenderResult<C, Q>render, const screen: Screen<typeof import("/vercel/path0/node_modules/.pnpm/@[email protected]/node_modules/@testing-library/dom/types/queries")>screen } from '@testing-library/svelte';
import 
const userEvent: {
    readonly setup: typeof setupMain;
    readonly clear: typeof clear;
    readonly click: typeof click;
    readonly copy: typeof copy;
    ... 12 more ...;
    readonly tab: typeof tab;
}
userEvent
from '@testing-library/user-event';
import { const expect: ExpectStaticexpect, const test: TestAPItest } from 'vitest'; import
type Component = SvelteComponent<Record<string, any>, any, any>
const Component: LegacyComponentType
Component
from './Component.svelte';
test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number | TestOptions): void (+2 overloads)test('Component', async () => { const const user: UserEventuser =
const userEvent: {
    readonly setup: typeof setupMain;
    readonly clear: typeof clear;
    readonly click: typeof click;
    readonly copy: typeof copy;
    ... 12 more ...;
    readonly tab: typeof tab;
}
userEvent
.setup: (options?: Options) => UserEventsetup();
render<SvelteComponent<Record<string, any>, any, any>, typeof import("/vercel/path0/node_modules/.pnpm/@[email protected]/node_modules/@testing-library/dom/types/queries")>(Component: ComponentType<...>, options?: SvelteComponentOptions<...> | undefined, renderOptions?: RenderOptions<...> | undefined): RenderResult<...>render(const Component: LegacyComponentTypeComponent); const const button: HTMLElementbutton = const screen: Screen<typeof import("/vercel/path0/node_modules/.pnpm/@[email protected]/node_modules/@testing-library/dom/types/queries")>screen.getByRole<HTMLElement>(role: ByRoleMatcher, options?: ByRoleOptions | undefined): HTMLElement (+1 overload)getByRole('button'); expect<HTMLElement>(actual: HTMLElement, message?: string): Assertion<HTMLElement> (+1 overload)expect(const button: HTMLElementbutton).toHaveTextContent(0); await const user: UserEventuser.click: (element: Element) => Promise<void>click(const button: HTMLElementbutton); expect<HTMLElement>(actual: HTMLElement, message?: string): Assertion<HTMLElement> (+1 overload)expect(const button: HTMLElementbutton).toHaveTextContent(1); });

When writing component tests that involve two-way bindings, context or snippet props, it’s best to create a wrapper component for your specific test and interact with that. @testing-library/svelte contains some examples.

E2E tests using Playwright

E2E (short for ‘end to end’) tests allow you to test your full application through the eyes of the user. This section uses Playwright as an example, but you can also use other solutions like Cypress or NightwatchJS.

You can use the Svelte CLI to setup Playwright either during project creation or later on. You can also set it up with npm init playwright. Additionally, you may also want to install an IDE plugin such as the VS Code extension to be able to execute tests from inside your IDE.

If you’ve run npm init playwright or are not using Vite, you may need to adjust the Playwright config to tell Playwright what to do before running the tests - mainly starting your application at a certain port. For example:

playwright.config
const 
const config: {
    webServer: {
        command: string;
        port: number;
    };
    testDir: string;
    testMatch: RegExp;
}
config
= {
webServer: {
    command: string;
    port: number;
}
webServer
: {
command: stringcommand: 'npm run build && npm run preview', port: numberport: 4173 }, testDir: stringtestDir: 'tests', testMatch: RegExptestMatch: /(.+\.)?(test|spec)\.[jt]s/ }; export default
const config: {
    webServer: {
        command: string;
        port: number;
    };
    testDir: string;
    testMatch: RegExp;
}
config
;

You can now start writing tests. These are totally unaware of Svelte as a framework, so you mainly interact with the DOM and write assertions.

tests/hello-world.spec
import { import expectexpect, import testtest } from '@playwright/test';

import testtest('home page has expected h1', async ({ page }) => {
	await page: anypage.goto('/');
	await import expectexpect(page: anypage.locator('h1')).toBeVisible();
});

Edit this page on llms.txt