Integration Tests using Vue 3 and Pinia

April 19, 2023


In this article, I will show you how to write integration tests for a Vue 3 app that uses Pinia for state management. I will be using this todo app by ar363 to demonstrate the process. I do this because I find it easier to explain things when I work with a concrete example.

Installing Dependencies

First of all, we need a test framework to run our tests. We'll use Vitest for this.

npm install --save-dev vitest

Next, we'll add the Vue Test Utils. This library provides a lot of useful functions to test Vue components comfortably.

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


Vitest uses Vite under the hood and uses the same configuration file. This is great because it means you don't have to duplicate your configuration between the two tools.

As we are working on a Vue 3 app, we'll need to ensure that the @vitejs/plugin-vue plugin is installed and used within our vite.config.ts configuration.

Add a vite.config.ts or a vite.config.js file to the root of your project.

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

export default defineConfig({
    plugins: [vue()]

Configuring Vitest

Next, we'll configure Vitest. This is done by creating a vitest.config.ts or a vitest.config.js file.

import { defineConfig, mergeConfig } from "vitest/config";
import viteConfig from "./vite.config";

export default mergeConfig(
    test: {
      environment: "jsdom", // Use jsdom environment, which is needed for testing components that use the DOM

By default, vitest looks for any files that match this pattern ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}']. This means that we can place our tests anywhere in our project as long as they match this pattern.

I like to put my tests in a separate folder, so I'll place them within a src/tests folder.

We'll also add the following script to our package.json file.

  "scripts": {
    "test": "vitest"

Writing the first Test

When writing integration tests for Single Page Applications, I like to use the root component as the starting point. I do this because the root component will include all other components, so testing this component will make sure that all components work together as expected.

Since this test doesn't have to know about the implementation details of any subcomponents (also known as "black box testing") it will be robust and have a good chance of catching real bugs.

We'll create a tests folder inside our src dir and add a HomeView.test.ts file.

describe("HomeView", () => {
  test("should be able to add and complete todos", async () => {
    const wrapper = mount(HomeView, {
      global: {
        plugins: [createPinia()],
    const todoInput = wrapper.find("[data-testid='todo-input']"); // Find the todo input
    const addTodoButton = wrapper.find("[data-testid='todo-add-button']"); // Find the add todo button

    // Create the two todos
    await todoInput.setValue("First Todo");
    await addTodoButton.trigger("click");
    await todoInput.setValue("Second Todo");
    await addTodoButton.trigger("click");

    const todos = wrapper.findAll("[data-testid='todo-item']"); // Find all open todo items

    // Check if there are two open todos

    // Check the first todo
    const doneTodos = wrapper.findAll("[data-testid='todo-item-done']"); // Find all done todo items

    // Check if there is one done todo
    expect(doneTodos.length).toBe(1); // Check if there is one done todo
    expect(wrapper.findAll("[data-testid='todo-item']").length).toBe(1); // Should still have one open todo

A few things to note here:

  • We use the mount function from @vue/test-utils to mount the component.
  • We use the createPinia function from pinia to create a new pinia instance. We don't mock our store, because we want to test the real store.
  • We use the data-testid attribute to find elements within our component. This makes our tests more robust. See the rationale behind this here.
  • We don't try to test the implementation details of our components. We test the component like a real user would.

You can find the full code for this example here.