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 component tests with 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:
npminstall-Dvitest
Then adjust your vite.config.js:
vite.config
import{defineConfig(alias)functiondefineConfig(config:UserConfig):UserConfig(+4overloads)importdefineConfig}from'vitest/config';exportdefaultdefineConfig(alias)defineConfig(config:UserConfig):UserConfig(+4overloads)importdefineConfig({ // ... // Tell Vitest to use the `browser` entry points in `package.json` files, even though it's running in Noderesolve(property)resolve?:AllResolveOptions|undefined:processvarprocess:NodeJS.Process.env(property)NodeJS.Process.env:NodeJS.ProcessEnv
The process.env property returns an object containing the user environment.
See environ(7).
It is possible to modify this object, but such modifications will not be
reflected outside the Node.js process, or (unless explicitly requested)
to other Worker threads.
In other words, the following example would not work:
Assigning a property on process.env will implicitly convert the value
to a string. This behavior is deprecated. Future versions of Node.js may
throw an error when the value is not a string, number, or boolean.
Unless explicitly specified when creating a Worker instance,
each Worker thread has its own copy of process.env, based on its
parent thread's process.env, or whatever was specified as the env option
to the Worker constructor. Changes to process.env will not be visible
across Worker threads, and only the main thread can make changes that
are visible to the operating system or to native add-ons. On Windows, a copy of process.env on a Worker instance operates in a case-sensitive manner
unlike the main thread.
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:
If the code being tested uses effects, you need to wrap the test inside $effect.root:
logger.svelte.test
import{flushSync}from'svelte';import{expect,test}from'vitest';import{logger}from'./logger.svelte.js';test('Effect',()=>{constcleanup=$effect.root(()=>{letcount=$state(0); // logger uses an $effect to log updates of its inputletlog=logger(()=>count); // effects normally run after a microtask, // use flushSync to execute all pending effects synchronouslyflushSync();expect(log).toEqual([0]);count=1;flushSync();expect(log).toEqual([0,1]);});cleanup();});
Runs code when a component is mounted to the DOM, and then whenever its dependencies change, i.e. $state or $derived values.
The timing of the execution is after the DOM has been updated.
Example:
$effect(()=>console.log('The count is now '+count));
If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
Runs code when a component is mounted to the DOM, and then whenever its dependencies change, i.e. $state or $derived values.
The timing of the execution is after the DOM has been updated.
Example:
$effect(()=>console.log('The count is now '+count));
If you return a function from the effect, it will be called right before the effect is run again, or when the component is unmounted.
It is possible to test your components in isolation, which allows you to render them in a browser (real or simulated), simulate behavior, and make assertions, without spinning up your whole app.
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):
:{ // If you are testing components client-side, you need to set up 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.environment(property)InlineConfig.environment?:VitestEnvironment|undefined
If used unsupported string, will try to load the package vitest-environment-${env}
'node'
:'jsdom'}, // Tell Vitest to use the `browser` entry points in `package.json` files, even though it's running in Noderesolve(property)resolve?:AllResolveOptions|undefined:processvarprocess:NodeJS.Process.env(property)NodeJS.Process.env:NodeJS.ProcessEnv
The process.env property returns an object containing the user environment.
See environ(7).
It is possible to modify this object, but such modifications will not be
reflected outside the Node.js process, or (unless explicitly requested)
to other Worker threads.
In other words, the following example would not work:
Assigning a property on process.env will implicitly convert the value
to a string. This behavior is deprecated. Future versions of Node.js may
throw an error when the value is not a string, number, or boolean.
Unless explicitly specified when creating a Worker instance,
each Worker thread has its own copy of process.env, based on its
parent thread's process.env, or whatever was specified as the env option
to the Worker constructor. Changes to process.env will not be visible
across Worker threads, and only the main thread can make changes that
are visible to the operating system or to native add-ons. On Windows, a copy of process.env on a Worker instance operates in a case-sensitive manner
unlike the main thread.
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:
Mounts a component to the given target and returns the exports and potentially the props (if compiled with accessors: true) of the component.
Transitions will play during the initial render unless the intro option is set to false.
Defines a test case with a given name and test function. The test function can optionally be configured with test options.
name
The name of the test or a function that will be used as a test name.
optionsOrFn
Optional. The test options or the test function if no explicit name is provided.
optionsOrTest
Optional. The test function or options, depending on the previous parameters.
{Error} If called inside another test function.
// Define a simple testtest('should add two numbers',()=>{expect(add(1,2)).toBe(3);});
// Define a test with optionstest('should subtract two numbers',{retry:3},()=>{expect(subtract(5,2)).toBe(3);});
('Component',()=>{ // Instantiate the component using Svelte's `mount` APIconstcomponentconstcomponent:{$on?(type:string,callback:(e:any)=>void):()=>void;$set?(props:Partial<Record<string,any>>):void;}&Record<string,any>=mount(alias)mount<Record<string,any>,{$on?(type:string,callback:(e:any)=>void):()=>void;$set?(props:Partial<Record<string,any>>):void;}&Record<string,any>>(component:ComponentType<SvelteComponent<Record<string,any>,any,any>>|Component<Record<string,any>,{$on?(type:string,callback:(e:any)=>void):()=>void;$set?(props:Partial<Record<string,any>>):void;}&Record<string,any>,any>,options:MountOptions<...>):{$on?(type:string,callback:(e:any)=>void):()=>void;$set?(props:Partial<Record<string,any>>):void;}&Record<...>importmount
Mounts a component to the given target and returns the exports and potentially the props (if compiled with accessors: true) of the component.
Transitions will play during the initial render unless the intro option is set to false.
The innerHTML property of the Element interface gets or sets the HTML or XML markup contained within the element, omitting any shadow roots in both cases.
The HTMLElement.click() method simulates a mouse click on an element. When called on an element, the element's click event is fired (unless its disabled attribute is set).
The innerHTML property of the Element interface gets or sets the HTML or XML markup contained within the element, omitting any shadow roots in both cases.
('<button>1</button>'); // Remove the component from the DOMunmount(alias)unmount(component:Record<string,any>,options?:{outro?:boolean;}|undefined):Promise<void>importunmount
Unmounts a component that was previously mounted using mount or hydrate.
Since 5.13.0, if options.outro is true, transitions will play before the component is removed from the DOM.
Returns a Promise that resolves after transitions have completed if options.outro is true, or immediately otherwise (prior to 5.13.0, returns void).
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:
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.
Component tests with Storybook
Storybook is a tool for developing and documenting UI components, and it can also be used to test your components. They’re run with Vitest’s browser mode, which renders your components in a real browser for the most realistic testing environment.
To get started, first install Storybook (using Svelte’s CLI) in your project via npx sv add storybook and choose the recommended configuration that includes testing features. If you’re already using Storybook, and for more information on Storybook’s testing capabilities, follow the Storybook testing docs to get started.
You can create stories for component variations and test interactions with the play function, which allows you to simulate behavior and make assertions using the Testing Library and Vitest APIs. Here’s an example of two stories that can be tested, one that renders an empty LoginForm component and one that simulates a user filling out the form:
LoginForm.stories
<scriptmodule>import{defineMeta}from'@storybook/addon-svelte-csf';import{expect,fn}from'storybook/test';importLoginFormfrom'./LoginForm.svelte';const{Story}=defineMeta({component:LoginForm,args:{// Pass a mock function to the `onSubmit` proponSubmit:fn(),}});</script><Storyname="Empty Form"/><Storyname="Filled Form"play={async({args,canvas,userEvent})=>{// Simulate a user filling out the formawaituserEvent.type(canvas.getByTestId('email'),'email@provider.com');awaituserEvent.type(canvas.getByTestId('password'),'a-random-password');awaituserEvent.click(canvas.getByRole('button'));// Run assertionsawaitexpect(args.onSubmit).toHaveBeenCalledTimes(1);awaitexpect(canvas.getByText('You’rein!')).toBeInTheDocument();}}/>
End-to-end tests with 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.
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
constconfigconstconfig:{webServer:{command:string;port:number;};testDir:string;testMatch:RegExp;}={webServer(property)webServer:{command:string;port:number;}:{command(property)command:string:'npm run build && npm run preview',port(property)port:number:4173},testDir(property)testDir:string:'tests',testMatch(property)testMatch:RegExp:/(.+\.)?(test|spec)\.[jt]s/};exportdefaultconfigconstconfig:{webServer:{command:string;port:number;};testDir:string;testMatch:RegExp;};
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{expectimportexpect,testimporttest}from'@playwright/test';testimporttest('home page has expected h1',async({page(parameter)page:any})=>{awaitpage(parameter)page:any.gotoany('/');awaitexpectimportexpect(page(parameter)page:any.locatorany('h1')).toBeVisibleany();});