Testing with Vitest

This article is a translation of the original article by Ignacio Falk “Testing with Vitest

I also run a telegram channelFrontend in a navy way”, where I talk about interesting things from the world of interface development.

Introduction

Vitest is a new testing environment based on Vite. It’s still in development and some features may not be ready yet, but it’s a good alternative to try and explore.

Setting

Let’s create a new Vite project!

Note. Vitest requires Vite >= v2.7.10 and Node >= v14.

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

Now that our project is created, we need to install all the dependencies needed 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 svelte plugin to it. Disable hot module replacement when running tests.

It should look like this:

import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'

export default defineConfig({
  plugins: [
    svelte({ hot: !process.env.VITEST }),
  ],
})

I use the VITEST env variable to separate the test and development environments, but if your configuration is too different, you can use a different test configuration file. There are several options for doing this.

  • Create a configuration file called vitest.config.ts: this 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 default Counter component in 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 test suite, let’s create a file called 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 a comment line @vitest-environment jsdom at the top of the file will allow us to mock the DOM API for all tests in the file. This can be avoided in every file with a config file. We can also make sure we are importing describe, it, expect globally. We do this also through the configuration file. Also we need to make types available by adding types vitest/globals to your tsconfig.json file (you can skip this if you’re 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"]
}

Now our test files don’t need to import global variables and we can remove the jsdom environment setting.

import { tick } from 'svelte';
import Counter from './Counter.svelte';

describe('Counter component', function () {
  // tests are the same
});

Teams

There are four commands to run from the cli:

  • dev: run vitest in development mode

  • related: runs tests against a list of source files

  • run: run tests once

  • watch: default mode, the same as when running vitest. Watch for changes and then re-run tests.

Test modifiers

There are test modifiers that will change the way your tests are run.

  • .only will focus on one or more tests, skipping the rest

  • .skip will skip the specified test

  • .todo will mark the test to be implemented later

  • .concurrently will run continuous tests marked as concurrent in parallel. This modifier can be combined with the previous ones. For example: it.concurrently.todo("сделать что-то асинхронное")

assertions

Vitest comes with chai and jest compatible assertions

expect(true).toBeTruthy() //ok
expect(1).toBe(Math.sqrt(4)) // false

For a list of available assertions, see API documentation.

Coating

For coverage reports, we will need to set c8 and run tests with the flag --coverage.

npm i -D c8

npx vitest --coverage

This will give us a good coverage report.

A coverage folder 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 the user interface, which will help you visualize the tests being run 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 lets you read the test code and open it in an editor.

More possibilities

Vitest comes with many other features such as snapshot testing, mocking, fake timers and more that you may know from other testing libraries.

Switching to Vitest (from Vite project using jest)

If you’re working on a small project or just starting out, you may need to adapt the config file and that’s it. If you’re using a function mock, Vitest uses TinySpy, and for fake timers, @sinonjs/fake-timers. Check compatibility. Also, don’t forget to import {vi} from vitest if you’ll be using it. Another thing you may need to tweak is the setup file. For example, to use the jest-dom matchers, we can create a setup file.

import '@testing-library/jest-dom'

and declare it in our config file.

export default defineConfig(({ mode }) => ({
    // ...
	test: {
		globals: true,
		environment: 'jsdom',
		setupFiles: ['<PATH_TO_SETUP_FILE>']
	}
}))

Here migration example VitePress on Vitest. (There are some changes in ts-config, but you can see where vitest is added and vitest.config.ts file)

Final Thoughts

Even though Vitest is still in development it looks very promising and the fact that they have kept an API very similar to Jest makes the migration very smooth. It also comes with TypeScript support (no external types package). Using the same (default) configuration file allows you to focus on writing tests very quickly. I’m looking forward to v1.0.0

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *