Vitest
is a new testing framework powered by Vite.
It's still in development, and some features may not be ready yet, but it's a nice alternative to try and explore.
Setup
Let's create a new Vite Project!
Note: Vitest
requires Vite >=v2.7.10 and Node >=v14 to work.
npm init vite@latest
✔ Project name: · try-vitest
✔ Select a framework: · svelte
✔ Select a variant: · svelte-ts
cd try-vitest
npm install //use the package manager you prefer
npm run dev
With our project created, we now need to install all the dependencies required for Vitest to work.
npm i -D vitest jsdom
I added jsdom
to be able to mock the DOM API.
By default, Vitest will use the configuration from vite.config.ts
.
I will add one modification to it, and it's svelte specific. Disabling Svelte's hot module replacement when running tests.
It should look like the following:
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
export default defineConfig({
plugins: [
svelte({ hot: !process.env.VITEST }),
],
})
I'm using the VITEST
env variable to differentiate when running tests, but if your configuration is too different, you can use another configuration file for tests.
There are a couple of options to do this.
- Create a configuration file named
vitest.config.ts
: it will take precedence when running tests - Using the
--config
flag: use it likenpx vitest --config <path_to_file>
Writing tests
Let's write some tests for the Counter component created by default with our project.
<script lang="ts">
let count: number = 0
const increment = () => {
count += 1
}
</script>
<button on:click={increment}>
Clicks: {count}
</button>
<style>
button {
font-family: inherit;
font-size: inherit;
padding: 1em 2em;
color: #ff3e00;
background-color: rgba(255, 62, 0, 0.1);
border-radius: 2em;
border: 2px solid rgba(255, 62, 0, 0);
outline: none;
width: 200px;
font-variant-numeric: tabular-nums;
cursor: pointer;
}
button:focus {
border: 2px solid #ff3e00;
}
button:active {
background-color: rgba(255, 62, 0, 0.2);
}
</style>
To write our first set of tests, let's create a file named Counter.spec.ts
next to our component.
// @vitest-environment jsdom
import { tick } from 'svelte';
import { describe, expect, it } from 'vitest';
import Counter from './Counter.svelte';
describe('Counter component', function () {
it('creates an instance', function () {
const host = document.createElement('div');
document.body.appendChild(host);
const instance = new Counter({ target: host });
expect(instance).toBeTruthy();
});
it('renders', function () {
const host = document.createElement('div');
document.body.appendChild(host);
new Counter({ target: host });
expect(host.innerHTML).toContain('Clicks: 0');
});
it('updates count when clicking a button', async function () {
const host = document.createElement('div');
document.body.appendChild(host);
new Counter({ target: host });
expect(host.innerHTML).toContain('Clicks: 0');
const btn = host.getElementsByTagName('button')[0];
btn.click();
await tick();
expect(host.innerHTML).toContain('Clicks: 1');
});
});
Adding the comment line @vitest-environment jsdom
at the top of the file will allow us to mock the DOM APIs for all of the tests in the file. However, doing this in every file can be avoided via the config file. We can also make sure that we import describe
, it
, expect
as globals. We do this via the config file. We need to make the types available by adding vitest/globals
types in your tsconfig.json
file (you can skip this if not using TypeScript).
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
export default defineConfig({
plugins: [svelte({ hot: !process.env.VITEST })],
test: {
globals: true,
environment: 'jsdom',
},
});
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"resolveJsonModule": true,
"baseUrl": ".",
"allowJs": true,
"checkJs": true,
/**
*Add the next line if using globals
*/
"types": ["vitest/globals"]
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
}
Our test files don't need to import globals now, and we can remove the jsdom environment setup.
import { tick } from 'svelte';
import Counter from './Counter.svelte';
describe('Counter component', function () {
// tests are the same
});
Commands
There are four commands to run from the cli:
dev
: run vitest in development moderelated
: runs tests for a list of source filesrun
: run tests oncewatch
: default mode, same as runningvitest
. Watches for changes and then reruns the tests.
test/suite modifiers
There are modifiers for tests and suites that will change how your tests run.
.only
will focus on one or more tests, skipping the rest. For suites, it will focus on all the tests in it..skip
will skip the specified test/suite..todo
will mark a test or suite to be implemented later..concurrently
will run contiguous tests marked as concurrent in parallel. For suites, it will run all tests in it in parallel. This modifier can be combined with the previous ones. For example:it.concurrently.todo("do something async")
Assertions
Vitest ships with chai and jest compatible assertions
expect(true).toBeTruthy() //ok
expect(1).toBe(Math.sqrt(4)) // false
For a list of available assertions, check the API docs.
Coverage
For coverage reports, we will need to install c8 and run the tests with the --coverage
flag
npm i -D c8
npx vitest --coverage
This will give us a nice coverage report.
An output folder coverage
will be created at the root of the project. You can specify the desired output type in the configuration file.
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte({ hot: !process.env.VITEST })],
test: {
globals: true,
environment: 'jsdom',
coverage:{
reporter:['text', 'json', 'html'] // change this property to the desired output
}
},
});
UI
You can also run vitest using a UI, which can help you visualize what tests you are running, and their results. Let's install the required package, and run it with the --ui
flag.
npm i -D @vitest/ui
npx vitest --ui
I like this interface. It even allows you to read the test code and open it in your editor.
More features
Vitest comes with many more features, like snapshot testing, mocking, fake timers, and more that you may know from other testing libraries.
Migrating to Vitest (from a Vite project using jest)
If you are working on a small project or just starting one, you may need to adapt your config file and that would be it. If you are using mock functions, Vitest uses TinySpy
and for fake timers, it uses @sinonjs/fake-timers
. Check for compatibility. Also, remember to import {vi} from vitest
if you will be using it.
Another thing that you may need to configure is a setup file.
For example, to use jest-dom
matchers, we can create a setup file.
import '@testing-library/jest-dom'
and declare it on our config file.
export default defineConfig(({ mode }) => ({
// ...
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['<PATH_TO_SETUP_FILE>']
}
}))
Here is an example of the migration of VitePress to Vitest
. (There are some ts-config changes but you can see where vitest is added, and the vitest.config.ts
file)
Final Thoughts
Even though Vitest is still in development, it looks very promising, and the fact that they kept the API so similar to Jest, makes the migration very smooth. It also ships with TypeScript support (no external types package). Using the same config file (by default) lets you focus on writing tests very quickly. I look forward to v1.0.0