Skip to content

Testing with Vitest

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 like npx 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 mode
  • related: runs tests for a list of source files
  • run: run tests once
  • watch: default mode, same as running vitest. 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.

Vitest 02

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.

Vitest UI

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