Getting Started with Vitest

What is Vitest?

Since Evan You’s build tool Vite has gained massive popularity, there’s now a blazing fast unit test framework that is powered by it: Vitest.

At a high level, Vitest is Jest-compatible and comes with out-of the-box ESM, Typescript, and JSX support, and it’s powered by esbuild. It uses the Vite dev server to transform your files during testing and listens to the same configuration of your app (through vite.config.js), thereby eliminating the duplication involved in using testing alternatives such as Jest.

In this Vitest tutorial, we’ll look at why you’d want to use Vitest and how to get started using it to test your components.


Why Vitest?

Vite is a build tool that aims to provide a faster and leaner development experience for modern web projects with out-of-the-box support for common web patterns, features like glob imports and SSR primitives. Its many plugins and integrations are fostering a vibrant ecosystem.

But this has led to a new problem: writing unit tests for applications that run on Vite.

Using frameworks like Jest with Vite has led to a lot of duplications in configuration between Vite and Jest, which Vitest solves for by removing the extra configuration chore needed to write unit tests for our applications. Vitest uses the same configuration as Vite, and shares a common transformation pipeline during development, build and test time. It is also extensible using the same plugin API as Vite and offers compatibility with Jest’s API for easy migration away from Jest without needing to do much refactoring.

As a result of all this, Vitest is very fast. So let’s get started using it.


How to use Vitest to test your components

How to install Vitest

Adding Vitest to your project requires that you have at least Vite >=v2.7.10 and Node >=v14 to work.

You can install Vitest using npm, yarn, or pnpm using the following commands in your terminal depending on your preference.

NPM

npm install -D vitest

YARN

yarn add -D vitest

PNPM

pnpm add -D vitest

Installing Vitest from the terminal


Vitest Configuration

After installing Vitest, the next thing we need to do is add it to our vite.config.js file:

vite.config.js

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
    plugins: [vue()],
    //add test to vite config
    test: {
        // ...
    },
});

Configuring Vitest for TypeScript is similar, but we need to add a reference to Vitest types using the triple slash command at the top of our config file if importing defineConfig from Vite.

/// <reference types="vitest" />
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// https://vitejs.dev/config/

export default defineConfig({
    plugins: [vue()],
    test: {
        // ...
    },
});

It’s important to note that Vitest can also be configured in your project by adding a vitest.config.js file in the root folder (same location as vite.config.js). If this file is present, it takes precedence over the vite.config.js for configuring Vitest. Vitest also allows for extra configuration which can be found in the config page.


Our Demo Example: A Notification

To see Vitest in action, we’re going to create a notifications component that displays three types of notifications: info, error, and success. Each state of this component looks like this.

info

info.gif

error

error.gif

success

Demo Vue component we’ll test with Vitest

Get familiarized with this component, because this is what we’ll be writing our tests for.

notification.vue

<template>
  <div
    :class="[
      'notification',
      type === 'error' ? 'notification--error' : null,
      type === 'success' ? 'notification--success' : null,
      type === 'info' ? 'notification--info' : null,
      message && message.length > 0 ? 'notification--slide' : null,
    ]"
  >
    <img
      src="https://res.cloudinary.com/djalafcj9/image/upload/v1634261166/getequityV2/denied_sbmv0e.png"
      v-if="type === 'error'"
    />
    <img
      src="https://res.cloudinary.com/djalafcj9/image/upload/v1656690265/getequityV2/Frame_irxz3e.png"
      v-if="type === 'success'"
    />
    <img
      src="https://res.cloudinary.com/djalafcj9/image/upload/v1634261166/getequityV2/pending_ctj1ke.png"
      v-if="type === 'info'"
    />
    <p class="notification__text">
      {{ message }}
    </p>
    <button
      ref="closeButton"
      class="notification__button"
      @click="$emit('clear-notification')"
    >
      <img
        src="https://res.cloudinary.com/djalafcj9/image/upload/v1635485821/getequityV2/close_muxdyb.png"
      />
    </button>
  </div>
</template>
<script>
  export default {
    name: "Notification",
    emits: ['clear-notification'],
    props: {
      type: {
        type: String,
        default: null,
      },
      message: {
        type: String,
        default: null,
      },
    },
  };
</script>

<style>
  .notification {
    transition: all 900ms ease-out;
    opacity: 0;
    z-index: 300001;
    transform: translateY(-100vh);
    box-sizing: border-box;
    padding: 10px 15px;
    width: 100%;
    max-width: 730px;
    /* margin: 0 auto; */
    display: flex;
    position: fixed;
    /* left: 0; */
    top: 20px;
    right: 15px;
    justify-content: flex-start;
    align-items: center;
    border-radius: 8px;
    min-height: 48px;
    box-sizing: border-box;
    color: #fff;
  }

  .notification--slide {
    transform: translateY(0px);
    opacity: 1;
  }

  .notification--error {
    background-color: #fdecec;
  }

  .notification__text {
    margin: 0;
    margin-left: 17px;
    margin-right: auto;
  }

  .notification--error .notification__text {
    color: #f03d3e;
  }

  .notification--success {
    background-color: #e1f9f2;
  }

  .notification--success > .notification__text {
    color: #146354;
  }

  .notification--info {
    background-color: #ffb647;
  }

  .notification__button {
    border: 0;
    background-color: transparent;
  }
</style>

Here, we create a component that displays a dynamic message using the message prop. We also make use of the type prop to style the background and text of this component, and display the different icons (error, success, and info) we plan to have using this type prop.

Finally, we have a button that is used to dismiss the notification by emitting a custom event: clear-notification.


What should we be testing?

Now that we have an understanding of the structure of the component that needs to be tested, we can ask ourselves what this component needs to do, to be functioning as intended.

Our tests need to check for the following:

  1. The component renders the correct style depending on the notification type.
  2. The notification fades away when message is empty.
  3. The component emits an event when the close button is clicked.

With these goals in mind, we can start fleshing out the test file for this component. Let’s add a new tests folder inside our project and create a file called notification.test.js inside it.


Installing our testing dependencies

When writing unit tests, there may be instances where we need to replace the existing implementation of the component with a dummy component that doesn’t really do anything. This is called a stub, and to use a stub in our tests, we’ll need access to the mount method from Vue Test Utils, the official testing utility library for Vue.js.

Let’s install Vue Test Utils now.

Installation

npm install --save-dev @vue/test-utils@next

# or

yarn add --dev @vue/test-utils@next

Now, inside our test file, we can import mount from "@vue/test-utils"

notification.test.js

import { mount } from "@vue/test-utils";

In our tests, we will also need to be able to mock the DOM. Vitest currently supports both happy-dom and jsdom. For this demo, we’re going with happy-dom, which we’ll also install now.

Installation

yarn add happy-dom --dev

After installation, we can either add the following comment to the top of our test file…

notification.test.js

/**
 * @vitest-environment happy-dom
 */

… or add this to the vite/vitest config file to avoid repetition when there is more than one test file that needs happy-dom to work.

vite.config.js

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// https://vitejs.dev/config/
export default defineConfig({
    plugins: [vue()],
    test: {
        environment: "happy-dom",
    },
});

Since we only have one test file, we can afford to go with the first option. At this point, our test file should look like this;

notification.test.js

/**
 * @vitest-environment happy-dom
 */

import { mount } from "@vue/test-utils";

With these dependencies in place, we can now import the component that we’re testing.

notification.test.js

/**
 * @vitest-environment happy-dom
 */

import { mount } from "@vue/test-utils";
import notification from "../components/notification.vue";

We’re now ready to start writing the tests for the different cases we identified earlier.


Common Vitest Methods

To write our tests, we need to make use of the following common methods, which can be imported from Vitest:

  • describe: This function accepts a name and a function and is used to group related tests together. It comes in handy when you’re writing tests for a component that has multiple test points (e.g logic and appearance).
  • test/it: This function represents the actual block of code that gets tested. It accepts a string which is usually the name or description of the test case (e.g renders the correct style for success) and another function where all the checks and testing take place.
  • expect: This function is used to test values or create assertions. It accepts an argument x that is expected to be an actual value (string, number, object, etc) and evaluates it using any of the supported methods (e.g toEqual(y) which checks if x is the same as y).

So let’s import these into our test file now

notification.test.js

/**
 * @vitest-environment happy-dom
 */

import { mount } from "@vue/test-utils";
import notification from "../components/notification.vue";
import { describe, expect, test } from "vitest";

With these functions accessible, let’s start constructing our unit tests.


Building our Vitest Unit Tests

We will first use the describe method to group our tests together.

notification.test.js

describe("notification.vue", () => {
    
});

Inside the describe block, we will add each of our actual tests.

As a reminder, our first test case is: The component renders the correct style depending on the notification type.

notification.test.js

describe("notification.vue", () => {
    test("renders the correct style for error", () => {

    });
});

Here, renders the correct style for error represents the name of what the test is checking for. It helps provide context into what the block of code is checking for so that it can be easily maintained and updated by someone other than the original author. It also makes it easy to identify a particular failing test case;

a list of test cases with a failing case

notification.test.js

describe("notification.vue", () => {
    test("renders the correct style for error", () => {
        const type = "error";
       
    });
});

If we go back to our component, we define a type prop that accepts a string that is used to determine things like background color, type of icon, and text color to render on the component. Here, we create a variable type and assign to it one of the types that we’re working with, error (from error, info, or success).

notification.test.js

describe("notification.vue", () => {
    test("renders the correct style for error", () => {
        const type = "error";
        const wrapper = mount(notification, {
            props: { type },
        });
    });
});

Here, we use mount to stub our component for the purpose of testing. mount accepts the component as the first argument and a list of options as the second argument. These options provide different properties that aim to ensure your components work as they would in your browser and from this list, we only need the props property. We make use of this property because our notification.vue component needs at least one prop for it to work effectively. After adding this option, we pass in the list of our props (in this case, we only have a type prop).

notification.test.js

describe("notification.vue", () => {
    test("renders the correct style for error", () => {
        const type = "error";
        const wrapper = mount(notification, {
            props: { type },
        });
        expect(wrapper.classes()).toEqual(
            expect.arrayContaining(["notification--error"])
        );
    });
});

At this point, all that is left is to write an assertion or better yet, the expected behavior of our component which is; renders the correct style for error, and to do this, we have made use of the expect method. It accepts our stubbed component with all the options (in our case, we’ve named it wrapper for easy reference). This method can be chained to a number of other methods but for this particular assertion, we’re checking that the component’s class list returns an array that includes this notification--error. We achieve this using the classes function which returns the array containing all the classes for the component. After this, the next thing would be to compare it using the toEqual function, which checks that a value X is equal to Y. Inside this function, we check that it returns an array that contains our class, notification--error.

If we repeat the same process for the other notification type; success and info, we would have the following;

import { mount } from "@vue/test-utils";
import notification from "../components/notification.vue";
import { describe, expect, test } from "vitest";
describe("notification.vue", () => {
    test("renders correct style for error", () => {
        const type = "error";
        const wrapper = mount(notification, {
            props: { type },
        });
        expect(wrapper.classes()).toEqual(
            expect.arrayContaining(["notification--error"])
        );
    });

    test("renders correct style for success", () => {
        const type = "success";
        const wrapper = mount(notification, {
            props: { type },
        });
        expect(wrapper.classes()).toEqual(
            expect.arrayContaining(["notification--success"])
        );
    });

    test("renders correct style for info", () => {
        const type = "info";
        const wrapper = mount(notification, {
            props: { type },
        });
        expect(wrapper.classes()).toEqual(
            expect.arrayContaining(["notification--info"])
        );
    });

    test("slides down when message is not empty", () => {
        const message = "success";
        const wrapper = mount(notification, {
            props: { message },
        });
        expect(wrapper.classes()).toEqual(
            expect.arrayContaining(["notification--slide"])
        );
    });
});

At this point, we’ve written tests to ensure our notifications are being styled according to their type. When users click on the close button on the component, we reset the message prop. According to our code, we’re adding or removing a notification--slide class depending on the value of this message prop as seen below.

notification.vue

<div
    :class="[
      'notification',
      type === 'error' ? 'notification--error' : null,
      type === 'success' ? 'notification--success' : null,
      type === 'info' ? 'notification--info' : null,
      message && message.length > 0 ? 'notification--slide' : null,
    ]"
  >
//...

If we’re to test for this particular assertion, it would look like this:

test("slides up when message is empty", () => {
        const message = "";
        const wrapper = mount(notification, {
            props: { message },
        });
        expect(wrapper.classes("notification--slide")).toBe(false);
    });

In this block of test code, we create a message variable with an empty string and pass it as a prop to our component. After this, we check the class array of our component and ensure it does not include the notification--slide class, which is responsible for making our component slide down/out into the user’s view. To do this, we use the toBe function, which takes a value A, and tries to check it is the same as B.

We also want to test that it emits an event whenever the button on the component is clicked.

test("emits event when close button is clicked", async() => {
        const wrapper = mount(notification, {
            data() {
                return {
                    clicked: false,
                };
            },
        });
        const closeButton = wrapper.find("button");
        await closeButton.trigger("click");
        expect(wrapper.emitted()).toHaveProperty("clear-notification");
    });

In this test block, we make use of an async function because we are going to be triggering an event, which returns a promise and we need to wait for this promise to resolve in order to catch the changes this event would cause. We also make use of the data function and add a clicked property which would be toggled when clicked.

At this point, we need to trigger this click event and we do so by first using the find function to get the button. This function is the same as querySelector as it accepts either a class, an id, or an attribute and returns an element.

After finding our button, we use the trigger method to trigger a click event. This method accepts the event name (click, focus, blur, keydown, etc.) that is to be triggered and performs this event and returns a promise. For this reason, we await this action in order to ensure the changes to our DOM have been made before we make an assertion based on this event.

Finally, we check the list of events that our component has emitted using the emitted method which returns an array. We then check that this array includes the clear-notification event.

Finally, we test to ensure that our component renders the right message being passed to the message prop. This is what we would have:

test("renders message when message is not empty", () => {
        const message = "Something happened, try again";
        const wrapper = mount(notification, {
            props: { message },
        });
        expect(wrapper.find("p").text()).toBe(message);
    });

Here, we create a message variable, assign a random string to it and pass it as a prop to our component. We then search for our message text using the p tag since this is where the message is being displayed and check that its text is the same as the message prop. p here can also be a class name or an attribute but we’re going with the tag name since there is only one paragraph tag in the component. We extract the content of this tag using text method, which is quite similar to innerText. Finally, we assert that this value would be the same as message using the same function from earlier, toBe.


Our complete test file

After covering all these, here is what the complete test file looks like.

notification.test.js

/**
 * @vitest-environment happy-dom
 */

import { mount } from "@vue/test-utils";
import notification from "../components/notification.vue";
import { describe, expect, test } from "vitest";

describe("notification.vue", () => {
    test("renders the correct style for error", () => {
        const type = "error";
        const wrapper = mount(notification, {
            props: { type },
        });
        expect(wrapper.classes()).toEqual(
            expect.arrayContaining(["notification--error"])
        );
    });

    test("renders the correct style for success", () => {
        const type = "success";
        const wrapper = mount(notification, {
            props: { type },
        });
        expect(wrapper.classes()).toEqual(
            expect.arrayContaining(["notification--success"])
        );
    });

    test("renders the correct style for info", () => {
        const type = "info";
        const wrapper = mount(notification, {
            props: { type },
        });
        expect(wrapper.classes()).toEqual(
            expect.arrayContaining(["notification--info"])
        );
    });

    test("slides down when message is not empty", () => {
        const message = "success";
        const wrapper = mount(notification, {
            props: { message },
        });
        expect(wrapper.classes()).toEqual(
            expect.arrayContaining(["notification--slide"])
        );
    });

    test("slides up when message is empty", () => {
        const message = "";
        const wrapper = mount(notification, {
            props: { message },
        });
        expect(wrapper.classes("notification--slide")).toBe(false);
    });

    test("emits event when close button is clicked", async() => {
        const wrapper = mount(notification, {
            data() {
                return {
                    clicked: false,
                };
            },
        });
        const closeButton = wrapper.find("button");
        await closeButton.trigger("click");
        expect(wrapper.emitted()).toHaveProperty("clear-notificatioon");
    });

    test("renders message when message is not empty", () => {
        const message = "Something happened, try again";
        const wrapper = mount(notification, {
            props: { message },
        });
        expect(wrapper.find("p").text()).toBe(message);
    });
});

A few things to note:

  1. We make use of mount for stubbing the component we’re trying to test, which is made available by Vue Test Utils. (yarn add --dev @vue/test-utils@next)

Running the tests

Now that we’re done writing our tests, we need to run them. To do this, we go to package.json and add the following lines to our script section:

package.json

"scripts": {
        "test": "vitest",
        "coverage": "vitest run --coverage"
    },

If we run yarn vitest or yarn test in our terminal, our test file gets run and we should see the test results and a breakdown.

Results of running our Vitest test

At this point, we have successfully run our first tests using Vitest. One thing to note from the result is that this command would only need to be run once and gets re-run as we make updates and changes to our test files, due to Vitest’s smart and instant watch mode.


Wrapping up

Using Vitest for unit testing your application is seamless and requires fewer steps to get up and running than alternatives like Jest. Vitest also makes it easy to migrate your existing tests from Jest to Vitest without a need for extra configuration.

If you’re interested in more testing content, check out Vue Mastery’s Unit Testing Vue 3 and Real World Testing courses.

Download the cheatsheets

Save time and energy with our cheat sheets, that give you all the essential syntax at your fingertips.

  • Vue 3 Composition API
  • Vue Essentials
  • Nuxt
  • Pinia
  • Vue Router
  • Vue 3 Migration Guide