Integration Tests using Vue 3 and Pinia
Tags: vue, vite, frontend
Published: 2023-04-19 17:25:51

## Introduction

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](https://github.com/ar363/todolist-vue-ts) by [ar363](https://github.com/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](https://vitest.dev/) for this.

```shell
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.

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

## Setup

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.

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

// https://vitejs.dev/config/
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.

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

export default mergeConfig(
  viteConfig,
  defineConfig({
    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.

```json
{
  "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.

```ts
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
    expect(todos.length).toBe(2);

    // Check the first todo
    await todos.at(0)?.setValue("checked");
    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](https://kentcdodds.com/blog/making-your-ui-tests-resilient-to-change).
- 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](https://github.com/carstenbehrens/todolist-vue-ts/commit/f9af99487dd9e6d64b7bc41b1f507bd440643f6d).