This tutorial will teach you how to mock Vue Router with Vitest in order to test the business logic that relies on navigation in your App.
Vue Router 4 is the official routing library for Vue 3. And since the introduction of the Composition API, there is a new functional approach to using it. Then how can we mock Vue Router with Vitest?
In simple terms, we have to replace the current implementation with a mock for the new functional methods, useRouter
and useRoute
. Comparatively, for global objects, $route
and $router
, we can supply them as globals when mounting the component.
Let’s see how we can achieve that by developing small test cases for an example application.
Prerequisites
Make sure that you have jsdom or happy-dom installed and set as the environment in the Vite configuration:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
test: {
environment: 'happy-dom',
},
})
In addition, you should also have vitest
and @vue/test-utils
installed.
The Example Application
Let’s imagine we have two example page components and routes:
Home.vue
will render for the root route and display a button that programmatically takes us to the greeting page.Greeting.vue
will render for the/greeting
route and accept a name query param that it greets.
The Home component will look like this:
<script setup>
import { useRouter } from 'vue-router'
import { RouteNames } from '../../router/router'
const router = useRouter()
function onClick() {
router.push({
name: RouteNames.GREETING,
query: {
name: 'John Doe',
},
})
}
</script>
<template>
<button @click="onClick">Go to Greeting</button>
</template>
And the Greeting component will look like this and will have a data-test-id
attribute on the H1 tag, that we will use later to select that element
<script setup>
import { useRoute } from 'vue-router'
import { RouteNames } from '../../router/router'
const route = useRoute()
</script>
<template>
<h1 data-test-id="greeting-message">Good morning {{ route.query.name }}</h1>
<RouterLink :to="{ name: RouteNames.HOME }">Go Back Home</RouterLink>
</template>
The router file will then contain the route names and map each route to the corresponding component. Afterward, it exports a new Router
instance:
import { createRouter, createWebHistory } from 'vue-router'
export const RouteNames = {
HOME: 'home',
GREETING: 'greeting',
}
const routes = [
{
path: '/',
name: RouteNames.HOME,
component: () => import('../views/Home/Home.vue'),
},
{
path: '/greeting',
name: RouteNames.GREETING,
component: () => import('../views/Greeting/Greeting.vue'),
},
]
export const router = createRouter({
routes,
history: createWebHistory(),
})
In main.js
we import the router and use it in the Vue app:
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { router } from './router/router'
const app = createApp(App)
app.use(router)
app.mount('#app')
Finally, in the App.vue
file we use <RouterView />
global component to render the component corresponding to the current route:
<template>
<div>
<RouterView />
</div>
</template>
When we run the app we should be able to navigate back and forth:
Let’s now write the unit tests for these two components.
How to Mock useRouter
Our first test will be for the Home
component where we’re using useRouter
to navigate to /greeting
. Since we’re using the Composition API, we have to mock useRouter
method.
The method returns an instance of the router object that we exported and registered in main.js
earlier.
First, create the spec file and specify the test case:
import { shallowMount } from '@vue/test-utils'
import { describe, test, expect } from 'vitest'
import { RouteNames } from '../../router/router'
import Home from './Home.vue'
describe('Home', () => {
test(`navigates to ${RouteNames.GREETING} route and sets query parameter name equal to 'John Doe'`, async () => {
const wrapper = shallowMount(Home)
await wrapper.find('button').trigger('click')
expect(something_to_happen)
})
})
Next, test whether the router push
method has been called with the correct parameters or not.
We don’t really care about the interaction between the router and the browser’s navigation API, which is already tested within the library itself.
Hence, import the required methods and mock the vue-router
package. Then mock the useRouter
function:
import { shallowMount } from '@vue/test-utils'
import { beforeEach, describe, test, vi, expect } from 'vitest'
import { useRouter } from 'vue-router'
import { RouteNames } from '../../router/router'
import Home from './Home.vue'
vi.mock('vue-router')
describe('Home', () => {
useRouter.mockReturnValue({
push: vi.fn(),
})
beforeEach(() => {
useRouter().push.mockReset()
})
test(`navigates to ${RouteNames.GREETING} route and sets query parameter name equal to 'John Doe'`, async () => {
const wrapper = shallowMount(Home)
await wrapper.find('button').trigger('click')
expect(something_to_happen)
})
})
As you can see, we mock it to return basically a mocked push method. now we can do multiple things with this mock. For instance, we can check if it has been called with some parameters
import { shallowMount } from '@vue/test-utils'
import { beforeEach, describe, test, vi, expect } from 'vitest'
import { useRouter } from 'vue-router'
import { RouteNames } from '../../router/router'
import Home from './Home.vue'
vi.mock('vue-router')
describe('Home', () => {
useRouter.mockReturnValue({
push: vi.fn(),
})
beforeEach(() => {
useRouter().push.mockReset()
})
test(`navigates to ${RouteNames.GREETING} route and sets query parameter name equal to 'John Doe'`, async () => {
const wrapper = shallowMount(Home)
await wrapper.find('button').trigger('click')
expect(useRouter().push).toHaveBeenCalledWith({
name: RouteNames.GREETING,
query: {
name: 'John Doe',
},
})
})
})
Notice that we use mockReset
within a beforeEach
block. That’s because we want to erase the call history before any new test case executes. Since leaving call history between units will introduce false positives if we use push
multiple times.
Finally, run the test, you should see that it passes.
How to Mock useRoute
In the Greeting
component, we are using useRoute
to extract the query param from the URL and display a greeting message, let’s develop then the test suite for this component.
First, as we did earlier create the spec file and write what the test should expect:
import { shallowMount } from '@vue/test-utils'
import { beforeEach, describe, test, vi, expect } from 'vitest'
import { useRoute } from 'vue-router'
import Greeting from './Greeting.vue'
vi.mock('vue-router')
describe('Greeting', () => {
const name = 'John Doe'
test('renders greeting for the name stored in query param "name"', () => {
const wrapper = shallowMount(Greeting)
expect(something_to_happen)
})
Next, mock the useRoute
function and supply the name as the query parameter:
import { shallowMount } from '@vue/test-utils'
import { beforeEach, describe, test, vi, expect } from 'vitest'
import { useRoute } from 'vue-router'
import Greeting from './Greeting.vue'
vi.mock('vue-router')
describe('Greeting', () => {
const name = 'John Doe'
useRoute.mockReturnValue({
query: {
name,
},
})
test('renders greeting for the name stored in query param "name"', () => {
const wrapper = shallowMount(Greeting)
expect(something_to_happen)
})
Finally, use the data-test-id
attribute on the h1 tag to select it and assert that the greeting message contains the name:
import { shallowMount } from '@vue/test-utils'
import { beforeEach, describe, test, vi, expect } from 'vitest'
import { useRoute } from 'vue-router'
import Greeting from './Greeting.vue'
vi.mock('vue-router')
describe('Greeting', () => {
const name = 'John Doe'
useRoute.mockReturnValue({
query: {
name,
},
})
test('renders greeting message for the URL query param "name"', () => {
const wrapper = shallowMount(Greeting)
expect(wrapper.find('[data-test-id="greeting-message"]').text()).toBe(`Good morning ${name}`)
})
})
One more thing, you might get a warning now saying RouterLink is not recognized as a vue component, that’s because we are mocking the vue-router library, to fix this, add a stubs array to the component options:
test('renders greeting message for the URL query param "name"', () => {
const wrapper = shallowMount(Greeting, {
global: {
stubs: ['RouterLink'],
},
})
expect(wrapper.find('[data-test-id="greeting-message"]').text()).toBe(`Good morning ${name}`)
})
Time for a test drive:
Mock $router and $route
First, adjust the Home
component to use $router
instead of useRouter
:
<script>
import { RouteNames } from '../../router/router'
export default {
methods: {
onClick() {
this.$router.push({
name: RouteNames.GREETING,
query: {
name: 'John Doe',
},
})
},
},
}
</script>
<template>
<button @click="onClick">Go to Greeting</button>
</template>
Next, adjust the test to mock the global objects, create a mock and add it to the globals.mock
import { shallowMount } from '@vue/test-utils'
import { beforeEach, describe, test, vi, expect } from 'vitest'
import { RouteNames } from '../../router/router'
import Home from './Home.vue'
describe('Home', () => {
let $routerMock = {
push: vi.fn(),
}
beforeEach(() => {
$routerMock.push.mockReset() // reset the mock between test cases
})
test(`navigates to ${RouteNames.GREETING} route and sets query parameter name equal to 'John Doe'`, async () => {
const wrapper = shallowMount(Home, {
global: {
mocks: {
$router: $routerMock,
},
},
})
await wrapper.find('button').trigger('click')
expect($routerMock.push).toHaveBeenCalledWith({
name: RouteNames.GREETING,
query: {
name: 'John Doe',
},
})
})
})
Similarly for the Greeting component, adjust it to use $route
:
<script setup>
import { RouteNames } from '../../router/router'
</script>
<template>
<h1 data-test-id="greeting-message">Good morning {{ $route.query.name }}</h1>
<RouterLink :to="{ name: RouteNames.HOME }">Go Back Home</RouterLink>
</template>
Then, adjust the test to use an object
import { shallowMount } from '@vue/test-utils'
import { beforeEach, describe, test, expect } from 'vitest'
import Greeting from './Greeting.vue'
describe('Greeting', () => {
const NAME = 'John Doe'
const $routeMock = {
query: {},
}
test('renders greeting message for the URL query param "name"', () => {
$routeMock.query = {
name: NAME,
}
const wrapper = shallowMount(Greeting, {
global: {
stubs: ['RouterLink'],
mocks: {
$route: $routeMock,
},
},
})
expect(wrapper.find('[data-test-id="greeting-message"]').text()).toBe(`Good morning ${NAME}`)
})
})
Finally, check that the tests are still passing
Conclusion
Thank you for spending the time to go through this tutorial, I hope you have learned something valuable. You can find the example code on Github.
If you are interested in learning more about Unit Testing and Vitest then please check my other Unit Testing Tutorials, cheers!
Also, make sure to follow me on Twitter and/or LinkedIn where I post tips and tricks!