CodingPR

Vue 3 Unit Testing with Vitest, and Vue Testing Utils.

Unit test your Vue 3 App with Vitest, TypeScript, and Vue Testing Utils.

Denisse Avatar

By:

Denisse Abreu
Nov. 6, 2022 7pm ET

Eng/Spa 10-min

Hello ๐Ÿ‘‹๐Ÿผ, Let's talk about testing. Testing is one of those subjects that, for whatever reason, is left out of most tutorials. Unit Testing is vital for the performance and operation of your application; it will ensure your App runs smoothly through its lifetime. Remember, as time passes, we update and change our App features, which can have breaking changes that only will be detected by running Tests. In this first part of the Testing tutorial, we'll work with Vue 3, Vitest, and Vue Testing Utils. We'll install a Vue 3 project and build a simple form component.
Let's get started!

First, install a Vue 3 project. If you don't know how to install Vue 3, follow Vue 3 installation guide. Open your terminal and run: npm init vue@latest.

Terminal
Project name: testing-tutorial Add TypeScript? Yes Add JSX Support? No Add Vue Router for Single Page Application development? Yes Add Pinia for state management? Yes Add Vitest for Unit testing? Yes
  • After the project installation, go to the root of the testing-tutorial, install the libraries and run the project in development mode.
Terminal
cd testing-tutorial npm install npm run dev

Run your First Unit Test.

Open the terminal and run your first unit test. You can find the command to run this test in the script part of the package.json manifest; It consists of a simple test to check if the HelloWorld.vue component mounts and pass a message prop. The test should pass.

Terminal
npm run test:unit

Test the front-page components by creating TheWelcome.spec.ts inside the __tests__ folder. In the following test, check that the component renders with an SVG and an anchor tag; Also, check that the anchor tag clicks.

src/components/__tests__/TheWelcome.spec.ts
import { mount } from "@vue/test-utils"; import { describe, expect, it, vi } from "vitest"; import TheWelcomeVue from "../TheWelcome.vue"; describe('The Welcome', () => { it('Mounts properly', async () => { const wrapper = mount(TheWelcomeVue) expect(wrapper).toBeTruthy() expect(wrapper.text()).toContain('Documentation') const svg = wrapper.find('svg') expect(svg).toBeTruthy() const a = wrapper.find('a') const spyOnA = vi.spyOn(a, 'trigger') await a.trigger('click') expect(spyOnA).toHaveBeenCalledOnce() }) })

How to test a Vue 3 form component?

We'll start the lesson by testing a simple form component. Go to the components folder, and create a form with one input element and a button. Import this component in the HelloWorld.vue file.

src/components/TheForm.vue
// Remember! to use script setup lang="ts" import { ref } from 'vue' const email = ref('') const Submit = () => { console.log(email.value) } <template> <form @submit.prevent="Submit"> <input v-model="email" type="email" placeholder="your@email.com" required /> <button type="submit">Submit</button> </form> </template>
  • Go to HelloWorld.spec.ts and add more test cases.
src/components/__tests__/HelloWorld.spec.ts
import { describe, it, expect, vi } from 'vitest'; import { mount } from '@vue/test-utils'; import TheFormVue from '../TheForm.vue'; function mountTheForm () { const wrapper = mount(TheFormVue, { props: {} }) return wrapper } describe('The Form', () => { it('Mounts properly', () => { expect(mountTheForm()).toBeTruthy() // Check the submit button mounts expect(mountTheForm().text()).toContain('Submit') }) it('click the submit button', async () => { const form = mountTheForm().find('form') // The spyOn function will report if the element // has been clicked. const spyOnForm = vi.spyOn(form, 'trigger') await form.trigger('click') // โŒwrong // expect(spyOnForm).toHaveBeenCalledTimes(2) // โœ… good expect(spyOnForm).toHaveBeenCalledOnce() }) it('Renders the input value', async () => { const input = mountTheForm().find('input') // input renders with an empty value expect(input.text()).toContain('') // fill the input await input.setValue('jane@doe.com') // Check the input has a value expect(input.element.value).toEqual('jane@doe.com') }) })

How to test The Vue Router?

Before testing the Vue Router, We'll do some refactoring. Go to the components folder, and create a Header.vue file. Move the header section of the App.vue into the newly created file. Your App.vue should look like the following code.

src/App.vue
import { RouterView } from 'vue-router' import Header from './components/Header.vue' <template> <Header /> <RouterView /> </template>
src/components/Header.vue
import { RouterLink } from 'vue-router' import HelloWorld from './HelloWorld.vue' <template> <header> <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" /> <div class="wrapper"> <HelloWorld msg="You did it!" /> <nav> <RouterLink id="link" to="/">Home</RouterLink> <RouterLink type="button" to="/about">About</RouterLink> </nav> </div> </header> </template>

Now that we have a good separation of concerns, start the Router Testing. In the __tests__ folder, create a Header.spec.ts and fill it with new test cases. Also, I added type="button" and id="link" to show you how to grab these elements.

src/components/__tests__/Header.spec.ts
import { describe, it, expect, test, vi } from 'vitest' import { mount } from '@vue/test-utils' import router from '@/router' import TheHeader from '../Header.vue' function mountTheHeader() { const wrapper = mount(TheHeader, { global: { plugins: [router] } }) return wrapper } describe('The Router', () => { it('mounts properly', () => { expect(mountTheHeader().text()).toContain('About') }) test('click the links', async () => { const push = vi.spyOn(router, 'push') await mountTheHeader().find('a[id=link]').trigger('click') expect(push).toHaveBeenCalledOnce() expect(push).toHaveBeenCalledWith('/') await mountTheHeader().find('a[type=button]').trigger('click') expect(push).toHaveBeenCalledTimes(2) expect(push).toHaveBeenCalledWith('/about') }) })

How to test Pinia?

In this part of the tutorial, we'll use Pinia, the state manager of Vue applications. We'll mock some values, send them to the store, and check that the values exists in the state. But first, let's test the counter store that Pinia installed by default.

Go to the stores folder and create __tests__. Inside that folder create, counter.spec.ts, and fill it with the following code. Remember to run your test!

src/stores/__tests__/counter.spec.ts
import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, test } from 'vitest' import { useCounterStore } from '../counter' describe('The counter store', () => { beforeEach(() => { setActivePinia(createPinia()) }) test('The counter', () => { let i = 0 const counter = useCounterStore() // Increment counter by 4 while (i < 4) { counter.increment() i++ } expect(counter.count).toBe(4) expect(counter.doubleCount).toBe(8) }) })
src/stores/counter.ts
import { ref, computed } from 'vue' import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', () => { const count = ref(0) const doubleCount = computed(() => count.value * 2) function increment() { count.value++ } return { count, doubleCount, increment } })

In the last part of this tutorial, we'll complicate the store and create a user store, where we insert some values and check that they are in the store. Go to the stores folder, and create user.ts.

src/stores/user.ts
import { defineStore } from "pinia"; import { reactive } from "vue"; interface UserData { email: string, name: string } export const useUserStore = defineStore('user', () => { const userData = reactive({ email: '', name:'' }) function insertData(userInput: UserData) { const { email, name } = userInput userData.email = email userData.name = name } return { userData, insertData} })
  • Create user.spec.ts in the __tests__ folder and run your test.
src/stores/user.spec.ts
import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, it } from 'vitest' import { ref } from 'vue' import { useUserStore } from '../user' describe('The user store', () => { beforeEach(() => { setActivePinia(createPinia()) }) it('has the user values', () => { const email = ref('jane@doe.com') const name = ref('Jane Doe') const userStore = useUserStore() userStore.insertData({ email: email.value, name: name.value }) expect(userStore.userData.email).toBe('jane@doe.com') expect(userStore.userData.name).toBe('Jane Doe') }) })