How to Mock Vue Router with Vitest

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.

vitest mock vue router - Home.vue

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:

vitest mock vue router - greeting.vue
😎

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

vitest vue router mock $route and $router
😎

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!

Amenallah Hsoumi
Amenallah Hsoumi

Senior Software Engineer, Indie Hacker, building cool stuff on the web!

Articles: 19