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
:
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.Process
process.NodeJS.Process.env: NodeJS.ProcessEnv
env.string | undefined
VITEST
? {
ResolveOptions.conditions?: string[] | undefined
conditions: ['browser']
}
: var undefined
undefined
});
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:
import { function flushSync<T = void>(fn?: (() => T) | undefined): T
flushSync } from 'svelte';
import { const expect: ExpectStatic
expect, const test: TestAPI
test } from 'vitest';
import { import multiplier
multiplier } from './multiplier.svelte.js';
test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number | TestOptions): void (+2 overloads)
test('Multiplier', () => {
let let double: any
double = import multiplier
multiplier(0, 2);
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)
expect(let double: any
double.value).JestAssertion<any>.toEqual: <number>(expected: number) => void
toEqual(0);
let double: any
double.set(5);
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)
expect(let double: any
double.value).JestAssertion<any>.toEqual: <number>(expected: number) => void
toEqual(10);
});
/**
* @param {number} initial
* @param {number} k
*/
export function function multiplier(initial: number, k: number): {
readonly value: number;
set: (c: number) => void;
}
multiplier(initial: number
initial, k: number
k) {
let let count: number
count = function $state<number>(initial: number): number (+1 overload)
namespace $state
$state(initial: number
initial);
return {
get value: number
value() {
return let count: number
count * k: number
k;
},
/** @param {number} c */
set: (c: number) => void
set: (c: number
c) => {
let count: number
count = c: number
c;
}
};
}
export function function multiplier(initial: number, k: number): {
readonly value: number;
set: (c: number) => void;
}
multiplier(initial: number
initial: number, k: number
k: number) {
let let count: number
count = function $state<number>(initial: number): number (+1 overload)
namespace $state
$state(initial: number
initial);
return {
get value: number
value() {
return let count: number
count * k: number
k;
},
set: (c: number) => void
set: (c: number
c: number) => {
let count: number
count = c: number
c;
}
};
}
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
:
import { function flushSync<T = void>(fn?: (() => T) | undefined): T
flushSync } from 'svelte';
import { const expect: ExpectStatic
expect, const test: TestAPI
test } from 'vitest';
import { import multiplier
multiplier } from './multiplier.svelte.js';
test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number | TestOptions): void (+2 overloads)
test('Multiplier', () => {
let let count: number
count = function $state<0>(initial: 0): 0 (+1 overload)
namespace $state
$state(0);
let let double: any
double = import multiplier
multiplier(() => let count: number
count, 2);
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)
expect(let double: any
double.value).JestAssertion<any>.toEqual: <number>(expected: number) => void
toEqual(0);
let count: number
count = 5;
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)
expect(let double: any
double.value).JestAssertion<any>.toEqual: <number>(expected: number) => void
toEqual(10);
});
/**
* @param {() => number} getCount
* @param {number} k
*/
export function function multiplier(getCount: () => number, k: number): {
readonly value: number;
}
multiplier(getCount: () => number
getCount, k: number
k) {
return {
get value: number
value() {
return getCount: () => number
getCount() * k: number
k;
}
};
}
export function function multiplier(getCount: () => number, k: number): {
readonly value: number;
}
multiplier(getCount: () => number
getCount: () => number, k: number
k: number) {
return {
get value: number
value() {
return getCount: () => number
getCount() * k: number
k;
}
};
}
If the code being tested uses effects, you need to wrap the test inside $effect.root
:
import { function flushSync<T = void>(fn?: (() => T) | undefined): T
flushSync } from 'svelte';
import { const expect: ExpectStatic
expect, const test: TestAPI
test } from 'vitest';
import { import logger
logger } from './logger.svelte.js';
test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number | TestOptions): void (+2 overloads)
test('Effect', () => {
const const cleanup: () => void
cleanup = namespace $effect
function $effect(fn: () => void | (() => void)): void
$effect.function $effect.root(fn: () => void | (() => void)): () => void
root(() => {
let let count: number
count = 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: any
log = import logger
logger(() => let count: number
count);
// effects normally run after a microtask,
// use flushSync to execute all pending effects synchronously
flushSync<void>(fn?: (() => void) | undefined): void
flushSync();
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)
expect(let log: any
log).JestAssertion<any>.toEqual: <number[]>(expected: number[]) => void
toEqual([0]);
let count: number
count = 1;
flushSync<void>(fn?: (() => void) | undefined): void
flushSync();
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)
expect(let log: any
log).JestAssertion<any>.toEqual: <number[]>(expected: number[]) => void
toEqual([0, 1]);
});
const cleanup: () => void
cleanup();
});
/**
* @param {() => any} getValue
*/
export function function logger(getValue: () => any): any[]
logger(getValue: () => any
getValue) {
/** @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[]): number
push(getValue: () => any
getValue());
});
return let log: any[]
log;
}
export function function logger(getValue: () => any): any[]
logger(getValue: () => any
getValue: () => 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[]): number
push(getValue: () => any
getValue());
});
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
:
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[] | undefined
plugins: [
/* ... */
],
UserConfig.test?: InlineConfig | undefined
test: {
// 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 | undefined
environment: '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.Process
process.NodeJS.Process.env: NodeJS.ProcessEnv
env.string | undefined
VITEST
? {
ResolveOptions.conditions?: string[] | undefined
conditions: ['browser']
}
: var undefined
undefined
});
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:
import { function flushSync<T = void>(fn?: (() => T) | undefined): T
flushSync, function mount<Props extends Record<string, any>, Exports extends Record<string, any>>(component: ComponentType<SvelteComponent<Props>> | Component<Props, Exports, any>, options: MountOptions<Props>): Exports
mount, function unmount(component: Record<string, any>, options?: {
outro?: boolean;
} | undefined): Promise<void>
unmount } from 'svelte';
import { const expect: ExpectStatic
expect, const test: TestAPI
test } 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: LegacyComponentType
Component, {
target: Document | Element | ShadowRoot
target: var document: Document
document.Document.body: HTMLElement
body, // `document` exists because of jsdom
props?: Record<string, any> | undefined
props: { initial: number
initial: 0 }
});
expect<string>(actual: string, message?: string): Assertion<string> (+1 overload)
expect(var document: Document
document.Document.body: HTMLElement
body.Element.innerHTML: string
innerHTML).JestAssertion<string>.toBe: <string>(expected: string) => void
toBe('<button>0</button>');
// Click the button, then flush the changes so you can synchronously write expectations
var document: Document
document.Document.body: HTMLElement
body.ParentNode.querySelector<"button">(selectors: "button"): HTMLButtonElement | null (+4 overloads)
querySelector('button').HTMLElement.click(): void
click();
flushSync<void>(fn?: (() => void) | undefined): void
flushSync();
expect<string>(actual: string, message?: string): Assertion<string> (+1 overload)
expect(var document: Document
document.Document.body: HTMLElement
body.Element.innerHTML: string
innerHTML).JestAssertion<string>.toBe: <string>(expected: string) => void
toBe('<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:
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: ExpectStatic
expect, const test: TestAPI
test } 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: UserEvent
user = 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) => UserEvent
setup();
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: LegacyComponentType
Component);
const const button: HTMLElement
button = 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: HTMLElement
button).toHaveTextContent(0);
await const user: UserEvent
user.click: (element: Element) => Promise<void>
click(const button: HTMLElement
button);
expect<HTMLElement>(actual: HTMLElement, message?: string): Assertion<HTMLElement> (+1 overload)
expect(const button: HTMLElement
button).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:
const const config: {
webServer: {
command: string;
port: number;
};
testDir: string;
testMatch: RegExp;
}
config = {
webServer: {
command: string;
port: number;
}
webServer: {
command: string
command: 'npm run build && npm run preview',
port: number
port: 4173
},
testDir: string
testDir: 'tests',
testMatch: RegExp
testMatch: /(.+\.)?(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.
import { import expect
expect, import test
test } from '@playwright/test';
import test
test('home page has expected h1', async ({ page }) => {
await page: any
page.goto('/');
await import expect
expect(page: any
page.locator('h1')).toBeVisible();
});