Hi 👋, I'm David Peng.
It's been a while since my last blog post.
In the last two months, I added 100+ unit/ component/ e2e tests in my Svelte project (yeah, I didn't do TDD because I wasn't familiar with testing enough 😅).
I experimented with different test runners, kept measuring DX, and tried to find my ideal toolset for testing. I'd love to share some of my learnings and thoughts here.
In this article, I'll focus on the basic setup of Vitest & Playwright to test our Svelte components. You can check the demo repo here: Svelte Component Test Demo Repo
In my next blog post, I'll also write about the advanced component test and mocking (Svelte runtime module & networking). Stay tuned!
Different types of test
Here's a brief introduction of each type (my point of view):
- Unit Test: Test the smallest piece of code, e.g., a simple function.
- Integration Test: Test group of functions, e.g., fetching and transforming data
- Component Test: Test the behavior/ transition of a UI component, e.g., form interaction, open a modal
- End-to-end (E2E) test: Complete user journey, e.g., login to a website and a series of operations
Test Runners
Vitest: A Vite-native unit test framework. (alternative: Jest, uvu)
You might hear more Svelte Developers migrating their tests from Jest to Vitest in the past few months.
Vitest is way faster than Jest because of Vite goodness, and it's a great fit with SvelteKit (use vite under the hood). Another plus is you can remove all your jest/ babel config 🎊.
A great read here: Testing a Svelte app with Vitest
Playwright: A framework for Web Testing and Automation. (alternative: Cypress)
Playwright is a recommended E2E test runner in the Svelte community. You can also find Playwright as an option in create-svelte
.
When to use each test runner?
Even though Vitest & Playwright are both test runners, they have a different focus and testing scenarios.
Vitest is incredibly fast because of its instant Hot Module Reload; it only reruns the test whenever the test, source code, or dependencies are changed, so it's suitable for the unit, integration tests.
While Playwright was created specifically for E2E testing, you need to build your Svelte app and start the server to test your app, which is much similar to the production.
But when it comes to the component test, we have a couple of choices:
- Unit testing runners like Vitest, Jest, or uvu + Testing Library
- Playwright/ Cypress component test (both powered by Vite)
- Storybook (I haven't tried it 😁)
Unit Test | ✨ Component Test ✨ | E2E Test |
---|---|---|
Vitest |
vitest + @testing-library/svelte or @playwright/experimental-ct-svelte
|
Playwright |
Let's see how to test the Svelte component using Vitest & Playwright.
Since we're going to talk about the component test, I like to share this clip from JS Party, you can also listen to the episode #233
Vitest + @testing-library/svelte
You can follow the below steps to setup Vitest or use the Svelte Adder: svelte-add-vitest
Take Svelte Demo App (TypeScript) for example
npm install -D vitest jsdom @testing-library/svelte
-
npm install -D @testing-library/jest-dom @types/testing-library__jest-dom
(optional but recommended) - Configure Vitest in
vite.config.js
:
import { sveltekit } from '@sveltejs/kit/vite';
/** @type {import('vite').UserConfig} */
const config = {
plugins: [sveltekit()],
/** Add below settings */
test: {
// Jest like globals
globals: true,
environment: 'jsdom',
include: ['src/**/*.{test,spec}.ts'],
// Extend jest-dom matchers
setupFiles: ['./setupTest.js']
}
};
export default config;
- Create
setupTest.js
and extendjest-dom
assertions/ matchers in Vitest:
import matchers from '@testing-library/jest-dom/matchers';
import { expect, vi } from 'vitest';
expect.extend(matchers);
The reason to use
jest-dom
assertions can be found here: Common mistakes with React Testing Library #Using the wrong assertion
- Add
types
intsconfig.json
:
{
"compilerOptions": {
"types": ["vitest/globals", "@testing-library/jest-dom"]
}
}
- Create
src/lib/Counter.test.ts
:
import { render, fireEvent, screen } from '@testing-library/svelte';
import Counter from './Counter.svelte';
describe('Test Counter.svelte', async () => {
it('Initial counter should be 0', async () => {
render(Counter);
expect(screen.getByText('0')).toBeInTheDocument();
});
it('Test decrease', async () => {
render(Counter);
const decreaseButton = screen.getByLabelText('Decrease the counter by one');
// Decrease by two
await fireEvent.click(decreaseButton);
await fireEvent.click(decreaseButton);
// Wait for animation
const counter = await screen.findByText('-2');
expect(counter).toBeInTheDocument();
});
it('Test increase', async () => {
render(Counter);
const increaseButton = screen.getByLabelText('Increase the counter by one');
// Increase by one
await fireEvent.click(increaseButton);
// Wait for animation
const counter = await screen.findByText('1');
expect(counter).toBeInTheDocument();
});
});
The benefit of using
screen
is you no longer need to keep the render call destructure up-to-date as you add/remove the queries you need. You only need to type screen. And let your editor's magic autocomplete take care of the rest. (ref: Common mistakes with React Testing Library #Not usingscreen
)
- Add
test
script inpackage.json
:
{
"scripts": {
"test": "vitest"
}
}
Playwright Experimental Component Test
This experimental component test is powered by Vite. For more details, please read the doc and the overview video from Playwright:
Take Svelte Demo App (TypeScript) for example
-
npm create svelte my-app
and choose the Demo Project npm install -D @playwright/experimental-ct-svelte
- Create
playwright-ct.config.ts
import type { PlaywrightTestConfig } from '@playwright/experimental-ct-svelte';
import { resolve } from 'node:path';
const config: PlaywrightTestConfig = {
testDir: 'tests/component',
use: {
ctViteConfig: {
resolve: {
alias: {
// Setup the built-in $lib alias in SvelteKit
$lib: resolve('src/lib')
}
}
}
}
};
export default config;
- Create several files in your Svelte project workspace:
playwright/index.html
<html lang="en">
<body>
<div id="root"></div>
<script type="module" src="/playwright/index.js"></script>
</body>
</html>
playwright/index.js
// Apply theme here, add anything your component needs at runtime here.
// Here, we import the demo project's css file
import '../src/app.css';
- Create
tests/component/Counter.test.ts
@playwright/experimental-ct-svelte
wrap @playwright/test
to provide an additional built-in component-testing specific fixture called mount
import { test, expect } from '@playwright/experimental-ct-svelte';
import Counter from '$lib/Counter.svelte';
test('Test Counter.svelte', async ({ mount }) => {
const component = await mount(Counter);
// Initial counter is "0"
await expect(component).toContainText("0");
// Decrease the counter
await component.locator('[aria-label="Decrease the counter by one"]').dblclick();
await expect(component).toContainText('-2');
// Increase the counter
await component.locator('[aria-label="Increase the counter by one"]').click();
await expect(component).toContainText('-1');
});
Your VS Code may show an error like this:
We can solve this by adding include
in your tsconfig.json
:
{
"include": ["tests/**/*.ts"]
}
- Add
test:com
script inpackage.json
{
"scripts": {
"test:com": "playwright test -c playwright-ct.config.ts"
}
}
Vitest (as test runner) + Playwright
I haven't tried this combination in my projects, but you can check the example from the Vitest GitHub repo: Use Playwright with Vitest as test runner
Wrapping Up
I've spent a considerable amount of time writing component tests using both Vitest & Playwright. Here are some of my thoughts and a reflection of DX (developer experience):
- Vitest + Testing Library: Setting up your testing environment takes more steps and dependencies. It also took me a while to grasp the concept and best practices of Testing Library and use query/ assertions properly, but once you have more confidence, you'd have a pleasant DX.
- Playwright Experimental Component Test: You can directly use most of Playwright's goodness like fixtures, network intercepting, in your component test, and its syntax are intuitive and pretty handy. While it's still in the experimental phase, getting more stable may take a while.
In my opinion, Vitest + Testing Library would probably be a better choice at this moment. Despite that Vitest hasn't hit 1.0, my personal experience of migrating from Jest is pretty smooth. Also, Testing Library is stable and used by many companies.
Thank you for your reading.
You can follow me on Twitter @davipon
Please leave your thoughts and experience below. Love to hear your feedback!
Resources
Discussion
sveltejs/kit: Vitest for unit testing #5285
Issues
Shim SvelteKit runtime import aliases / Importing $app/* fails #1485
Articles
- Unit Testing Svelte Components (Svelte Society)
- Testing a Svelte app with Vitest
- Component Driven User Interfaces
Top comments (4)
Great post!
However, I can't seem to get Playwright to mount Svelte component properly. Wondering if you had similar issue?
And this is my App.svelte
and Playwright Config
I did encounter the same problem, it turned out that we can't add
plugins: [svelte()]
underctViteConfig
.There are some bugs/ conflicts after recent breaking changes of SvelteKit and Vite 3.0.
Interesting. I don't actually use SvelteKit but just plain Svelte app.
So, have you found the way around the issue or how did you got it to work in your original post?
If you use the
playwright-ct.config.ts
in my original post, it should work.