TLDR
This guide shows you how to test Vue Router components using real router integration and isolated component testing with mocks. You’ll learn to verify router-link interactions, programmatic navigation, and navigation guard handling.
Introduction
Modern Vue applications need thorough testing to ensure reliable navigation and component performance. We’ll cover testing strategies using Testing Library and Vitest to simulate real-world scenarios through router integration and component isolation.
Vue Router Testing Techniques with Testing Library and Vitest
Let’s explore how to write effective tests for Vue Router components using both real router instances and mocks.
Testing Vue Router Navigation Components
Navigation Component Example
<!-- NavigationMenu.vue -->
<script setup lang="ts">
import { import useRouter
useRouter } from 'vue-router'
const const router: any
router = import useRouter
useRouter()
const const goToProfile: () => void
goToProfile = () => {
const router: any
router.push('/profile')
}
</script>
<template>
<nav: HTMLAttributes & ReservedProps
nav>
<type RouterLink: unknown
router-link to: string
to="/dashboard" class: string
class="nav-link">Dashboard</router-link>
<type RouterLink: unknown
router-link to: string
to="/settings" class: string
class="nav-link">Settings</router-link>
<button: ButtonHTMLAttributes & ReservedProps
button @onClick?: ((payload: MouseEvent) => void) | undefined
click="const goToProfile: () => void
goToProfile">Profile</button: ButtonHTMLAttributes & ReservedProps
button>
</nav: HTMLAttributes & ReservedProps
nav>
</template>
Real Router Integration Testing
Test complete routing behavior with a real router instance:
import { import render
render, import screen
screen } from '@testing-library/vue'
import { import describe
describe, import it
it, import expect
expect } from 'vitest'
import { import createRouter
createRouter, import createWebHistory
createWebHistory } from 'vue-router'
import import NavigationMenu
NavigationMenu from '../NavigationMenu..vue'
import { import userEvent
userEvent } from '@testing-library/user-event'
import describe
describe('NavigationMenu', () => {
import it
it('should navigate using router links', async () => {
const const router: any
router = import createRouter
createRouter({
history: any
history: import createWebHistory
createWebHistory(),
routes: {
path: string;
component: {
template: string;
};
}[]
routes: [
{ path: string
path: '/dashboard', component: {
template: string;
}
component: { template: string
template: 'Dashboard' } },
{ path: string
path: '/settings', component: {
template: string;
}
component: { template: string
template: 'Settings' } },
{ path: string
path: '/profile', component: {
template: string;
}
component: { template: string
template: 'Profile' } },
{ path: string
path: '/', component: {
template: string;
}
component: { template: string
template: 'Home' } },
],
})
import render
render(import NavigationMenu
NavigationMenu, {
global: {
plugins: any[];
}
global: {
plugins: any[]
plugins: [const router: any
router],
},
})
const const user: any
user = import userEvent
userEvent.setup()
import expect
expect(const router: any
router.currentRoute.value.path).toBe('/')
await const router: any
router.isReady()
await const user: any
user.click(import screen
screen.getByText('Dashboard'))
import expect
expect(const router: any
router.currentRoute.value.path).toBe('/dashboard')
await const user: any
user.click(import screen
screen.getByText('Profile'))
import expect
expect(const router: any
router.currentRoute.value.path).toBe('/profile')
})
})
Mocked Router Testing
Test components in isolation with router mocks:
import { import render
render, import screen
screen } from '@testing-library/vue'
import { import useRouter
useRouter, type import Router
Router } from 'vue-router'
import { import describe
describe, import it
it, import expect
expect, import vi
vi } from 'vitest'
import import NavigationMenu
NavigationMenu from '../NavigationMenu..vue'
import import userEvent
userEvent from '@testing-library/user-event'
const const mockPush: any
mockPush = import vi
vi.fn()
import vi
vi.mock('vue-router', () => ({
useRouter: any
useRouter: import vi
vi.fn(),
}))
import describe
describe('NavigationMenu with mocked router', () => {
import it
it('should handle navigation with mocked router', async () => {
const const mockRouter: Router
mockRouter = {
push: any
push: const mockPush: any
mockPush,
currentRoute: {
value: {
path: string;
};
}
currentRoute: { value: {
path: string;
}
value: { path: string
path: '/' } },
} as unknown as import Router
Router
import vi
vi.mocked(import useRouter
useRouter).mockImplementation(() => const mockRouter: Router
mockRouter)
const const user: any
user = import userEvent
userEvent.setup()
import render
render(import NavigationMenu
NavigationMenu)
await const user: any
user.click(import screen
screen.getByText('Profile'))
import expect
expect(const mockPush: any
mockPush).toHaveBeenCalledWith('/profile')
})
})
RouterLink Stub for Isolated Testing
Create a RouterLink stub to test navigation without router-link behavior:
// test-utils.ts
import { type Component<Props = any, RawBindings = any, D = any, C extends ComputedOptions = ComputedOptions, M extends MethodOptions = MethodOptions, E extends EmitsOptions | Record<string, any[]> = {}, S extends Record<string, any> = any> = ConcreteComponent<Props, RawBindings, D, C, M, E, S> | ComponentPublicInstanceConstructor<Props, any, any, any, ComputedOptions, MethodOptions>
A type used in public APIs where a component type is expected.
The constructor type is an artificial type returned by defineComponent().Component, function h<K extends keyof HTMLElementTagNameMap>(type: K, children?: RawChildren): VNode (+22 overloads)
h } from 'vue'
import { import useRouter
useRouter } from 'vue-router'
export const const RouterLinkStub: Component
RouterLinkStub: type Component<Props = any, RawBindings = any, D = any, C extends ComputedOptions = ComputedOptions, M extends MethodOptions = MethodOptions, E extends EmitsOptions | Record<string, any[]> = {}, S extends Record<string, any> = any> = ConcreteComponent<Props, RawBindings, D, C, M, E, S> | ComponentPublicInstanceConstructor<Props, any, any, any, ComputedOptions, MethodOptions>
A type used in public APIs where a component type is expected.
The constructor type is an artificial type returned by defineComponent().Component = {
name: string
name: 'RouterLinkStub',
FunctionalComponent<any, {}, any, {}>.props?: ComponentPropsOptions<any> | undefined
props: {
to: {
type: (StringConstructor | ObjectConstructor)[];
required: true;
}
to: {
PropOptions<any, any>.type?: true | PropType<any> | null | undefined
type: [var String: StringConstructor
Allows manipulation and formatting of text strings and determination and location of substrings within strings.String, var Object: ObjectConstructor
Provides functionality common to all JavaScript objects.Object],
PropOptions<any, any>.required?: boolean | undefined
required: true,
},
tag: {
type: StringConstructor;
default: string;
}
tag: {
PropOptions<any, any>.type?: true | PropType<any> | null | undefined
type: var String: StringConstructor
Allows manipulation and formatting of text strings and determination and location of substrings within strings.String,
PropOptions<any, any>.default?: any
default: 'a',
},
exact: BooleanConstructor
exact: var Boolean: BooleanConstructor
Boolean,
exactPath: BooleanConstructor
exactPath: var Boolean: BooleanConstructor
Boolean,
append: BooleanConstructor
append: var Boolean: BooleanConstructor
Boolean,
replace: BooleanConstructor
replace: var Boolean: BooleanConstructor
Boolean,
activeClass: StringConstructor
activeClass: var String: StringConstructor
Allows manipulation and formatting of text strings and determination and location of substrings within strings.String,
exactActiveClass: StringConstructor
exactActiveClass: var String: StringConstructor
Allows manipulation and formatting of text strings and determination and location of substrings within strings.String,
exactPathActiveClass: StringConstructor
exactPathActiveClass: var String: StringConstructor
Allows manipulation and formatting of text strings and determination and location of substrings within strings.String,
event: {
type: (StringConstructor | ArrayConstructor)[];
default: string;
}
event: {
PropOptions<any, any>.type?: true | PropType<any> | null | undefined
type: [var String: StringConstructor
Allows manipulation and formatting of text strings and determination and location of substrings within strings.String, var Array: ArrayConstructor
Array],
PropOptions<any, any>.default?: any
default: 'click',
},
},
ComponentOptionsBase<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions>.setup?: ((this: void, props: LooseRequired<any>, ctx: {
attrs: Data;
slots: Readonly<InternalSlots>;
emit: ((event: unknown, ...args: any[]) => void) | ((event: string, ...args: any[]) => void);
expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed) => void;
}) => any) | undefined
setup(props: LooseRequired<any>
props) {
const const router: any
router = import useRouter
useRouter()
const const navigate: () => void
navigate = () => {
const router: any
router.push(props: LooseRequired<any>
props.to)
}
return { navigate: () => void
navigate }
},
ComponentOptionsBase<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions>.render?: Function | undefined
render() {
return h<any>(type: any, props?: (RawProps & HTMLElementEventHandler) | null, children?: RawChildren | RawSlots): VNode (+22 overloads)
h(
this.tag,
{
onClick?: ((ev: MouseEvent) => any) | undefined
onClick: () => this.navigate(),
},
this.$slots.default?.(),
)
},
}
Use the RouterLinkStub in tests:
import { import render
render, import screen
screen } from '@testing-library/vue'
import { import useRouter
useRouter, type import Router
Router } from 'vue-router'
import { import describe
describe, import it
it, import expect
expect, import vi
vi } from 'vitest'
import import NavigationMenu
NavigationMenu from '../NavigationMenu..vue'
import import userEvent
userEvent from '@testing-library/user-event'
import { import RouterLinkStub
RouterLinkStub } from './test-utils'
const const mockPush: any
mockPush = import vi
vi.fn()
import vi
vi.mock('vue-router', () => ({
useRouter: any
useRouter: import vi
vi.fn(),
}))
import describe
describe('NavigationMenu with mocked router', () => {
import it
it('should handle navigation with mocked router', async () => {
const const mockRouter: Router
mockRouter = {
push: any
push: const mockPush: any
mockPush,
currentRoute: {
value: {
path: string;
};
}
currentRoute: { value: {
path: string;
}
value: { path: string
path: '/' } },
} as unknown as import Router
Router
import vi
vi.mocked(import useRouter
useRouter).mockImplementation(() => const mockRouter: Router
mockRouter)
const const user: any
user = import userEvent
userEvent.setup()
import render
render(import NavigationMenu
NavigationMenu, {
global: {
stubs: {
RouterLink: any;
};
}
global: {
stubs: {
RouterLink: any;
}
stubs: {
type RouterLink: any
RouterLink: import RouterLinkStub
RouterLinkStub,
},
},
})
await const user: any
user.click(import screen
screen.getByText('Dashboard'))
import expect
expect(const mockPush: any
mockPush).toHaveBeenCalledWith('/dashboard')
})
})
Testing Navigation Guards
Test navigation guards by rendering the component within a route context:
<script setup lang="ts">
import { import onBeforeRouteLeave
onBeforeRouteLeave } from 'vue-router'
import onBeforeRouteLeave
onBeforeRouteLeave(() => {
return var window: Window & typeof globalThis
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/window)window.function confirm(message?: string): boolean
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/confirm)confirm('Do you really want to leave this page?')
})
</script>
<template>
<div: HTMLAttributes & ReservedProps
div>
<h1: HTMLAttributes & ReservedProps
h1>Route Leave Guard Demo</h1: HTMLAttributes & ReservedProps
h1>
<div: HTMLAttributes & ReservedProps
div>
<nav: HTMLAttributes & ReservedProps
nav>
<type RouterLink: unknown
router-link to: string
to="/">Home</router-link> |
<type RouterLink: unknown
router-link to: string
to="/about">About</router-link> |
<type RouterLink: unknown
router-link to: string
to="/guard-demo">Guard Demo</router-link>
</nav: HTMLAttributes & ReservedProps
nav>
</div: HTMLAttributes & ReservedProps
div>
</div: HTMLAttributes & ReservedProps
div>
</template>
Test the navigation guard:
import { import render
render, import screen
screen } from '@testing-library/vue'
import import userEvent
userEvent from '@testing-library/user-event'
import { import describe
describe, import it
it, import expect
expect, import vi
vi, import beforeEach
beforeEach } from 'vitest'
import { import createRouter
createRouter, import createWebHistory
createWebHistory } from 'vue-router'
import import RouteLeaveGuardDemo
RouteLeaveGuardDemo from '../RouteLeaveGuardDemo.vue'
const const routes: {
path: string;
component: any;
}[]
routes = [
{ path: string
path: '/', component: any
component: import RouteLeaveGuardDemo
RouteLeaveGuardDemo },
{ path: string
path: '/about', component: {
template: string;
}
component: { template: string
template: '<div>About</div>' } },
]
const const router: any
router = import createRouter
createRouter({
history: any
history: import createWebHistory
createWebHistory(),
routes: {
path: string;
component: any;
}[]
routes,
})
const const App: {
template: string;
}
App = { template: string
template: '<router-view />' }
import describe
describe('RouteLeaveGuardDemo', () => {
import beforeEach
beforeEach(async () => {
import vi
vi.clearAllMocks()
var window: Window & typeof globalThis
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/window)window.function confirm(message?: string): boolean
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/confirm)confirm = import vi
vi.fn()
await const router: any
router.push('/')
await const router: any
router.isReady()
})
import it
it('should prompt when guard is triggered and user confirms', async () => {
// Set window.confirm to simulate a user confirming the prompt
var window: Window & typeof globalThis
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/window)window.function confirm(message?: string): boolean
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/confirm)confirm = import vi
vi.fn(() => true)
// Render the component within a router context
import render
render(const App: {
template: string;
}
App, {
global: {
plugins: any[];
}
global: {
plugins: any[]
plugins: [const router: any
router],
},
})
const const user: any
user = import userEvent
userEvent.setup()
// Find the 'About' link and simulate a user click
const const aboutLink: any
aboutLink = import screen
screen.getByRole('link', { name: RegExp
name: /About/i })
await const user: any
user.click(const aboutLink: any
aboutLink)
// Assert that the confirm dialog was shown with the correct message
import expect
expect(var window: Window & typeof globalThis
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/window)window.function confirm(message?: string): boolean
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/confirm)confirm).toHaveBeenCalledWith('Do you really want to leave this page?')
// Verify that the navigation was allowed and the route changed to '/about'
import expect
expect(const router: any
router.currentRoute.value.path).toBe('/about')
})
})
Reusable Router Test Helper
Create a helper function to simplify router setup:
// test-utils.ts
import { import render
render } from '@testing-library/vue'
import { import createRouter
createRouter, import createWebHistory
createWebHistory } from 'vue-router'
import type { import RenderOptions
RenderOptions } from '@testing-library/vue'
// path of the definition of your routes
import { import routes
routes } from '../../router/index.ts'
interface RenderWithRouterOptions extends type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P]; }
Construct a type with the properties of T except for those in type K.Omit<import RenderOptions
RenderOptions<any>, 'global'> {
RenderWithRouterOptions.initialRoute?: string | undefined
initialRoute?: string
RenderWithRouterOptions.routerOptions?: {
routes?: typeof routes;
history?: ReturnType<typeof createWebHistory>;
} | undefined
routerOptions?: {
routes?: any
routes?: typeof import routes
routes
history?: any
history?: type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
Obtain the return type of a function typeReturnType<typeof import createWebHistory
createWebHistory>
}
}
export function function renderWithRouter(Component: any, options?: RenderWithRouterOptions): any
renderWithRouter(type Component: any
Component: any, options: RenderWithRouterOptions
options: RenderWithRouterOptions = {}) {
const { const initialRoute: string
initialRoute = '/', const routerOptions: {
routes?: typeof routes;
history?: ReturnType<typeof createWebHistory>;
}
routerOptions = {}, ...const renderOptions: {
[x: string]: RenderOptions<any>;
[x: number]: RenderOptions<any>;
[x: symbol]: RenderOptions<any>;
}
renderOptions } = options: RenderWithRouterOptions
options
const const router: any
router = import createRouter
createRouter({
history: any
history: import createWebHistory
createWebHistory(),
// Use provided routes or import from your router file
routes: any
routes: const routerOptions: {
routes?: typeof routes;
history?: ReturnType<typeof createWebHistory>;
}
routerOptions.routes?: any
routes || import routes
routes,
})
const router: any
router.push(const initialRoute: string
initialRoute)
return {
// Return everything from regular render, plus the router instance
...import render
render(type Component: any
Component, {
global: {
plugins: any[];
}
global: {
plugins: any[]
plugins: [const router: any
router],
},
...const renderOptions: {
[x: string]: RenderOptions<any>;
[x: number]: RenderOptions<any>;
[x: symbol]: RenderOptions<any>;
}
renderOptions,
}),
router: any
router,
}
}
Use the helper in tests:
describe('NavigationMenu', () => {
it('should navigate using router links', async () => {
const { const router: any
router } = renderWithRouter(NavigationMenu, {
initialRoute: string
initialRoute: '/',
})
await const router: any
router.isReady()
const const user: any
user = userEvent.setup()
await const user: any
user.click(var screen: Screen
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/screen)screen.getByText('Dashboard'))
expect(const router: any
router.currentRoute.value.path).toBe('/dashboard')
})
})
Conclusion: Best Practices for Vue Router Component Testing
When we test components that rely on the router, we need to consider whether we want to test the functionality in the most realistic use case or in isolation. In my humble opinion, the more you mock a test, the worse it will get. My personal advice would be to aim to use the real router instead of mocking it. Sometimes, there are exceptions, so keep that in mind.
Also, you can help yourself by focusing on components that don’t rely on router functionality. Reserve router logic for view/page components. While keeping our components simple, we will never have the problem of mocking the router in the first place.