原文地址:How to Test Vue Router Components with Testing Library and Vitest,作者是Alex,一位来自德国的开发人员。在 25 年的 Vue Nation
上有演讲。
介绍
现代 Vue 应用程序需要全面测试,以确保可靠的导航和组件性能。我们将介绍使用 测试库和 Vitest 的测试策略,通过路由集成和组件隔离来模拟真实场景。
使用测试库和Vitest 进行 Vue Router 的测试技术
让我们探索如何使用真实路由实例和模拟为 Vue Router 组件编写有效的测试。
测试 Vue Router 导航组件
导航组件示例
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
function goToProfile() {
router.push('/profile')
}
</script>
<template>
<nav>
<router-link to="/dashboard" class="nav-link">
Dashboard
</router-link>
<router-link to="/settings" class="nav-link">
Settings
</router-link>
<button @click="goToProfile">
Profile
</button>
</nav>
</template>
真正的路由集成测试
使用真实的路由器实例测试完整的路由行为:
import { userEvent } from '@testing-library/user-event'
import { render, screen } from '@testing-library/vue'
import { describe, expect, it } from 'vitest'
import { createRouter, createWebHistory } from 'vue-router'
import NavigationMenu from '../NavigationMenu..vue'
describe('NavigationMenu', () => {
it('should navigate using router links', async () => {
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/dashboard', component: { template: 'Dashboard' } },
{ path: '/settings', component: { template: 'Settings' } },
{ path: '/profile', component: { template: 'Profile' } },
{ path: '/', component: { template: 'Home' } },
],
})
render(NavigationMenu, {
global: {
plugins: [router],
},
})
const user = userEvent.setup()
expect(router.currentRoute.value.path).toBe('/')
await router.isReady()
await user.click(screen.getByText('Dashboard'))
expect(router.currentRoute.value.path).toBe('/dashboard')
await user.click(screen.getByText('Profile'))
expect(router.currentRoute.value.path).toBe('/profile')
})
})
模拟路由测试
使用 router mock 隔离测试组件
import type { Router } from 'vue-router';
import userEvent from '@testing-library/user-event'
import { render, screen } from '@testing-library/vue'
import { describe, expect, it, vi } from 'vitest'
import { useRouter } from 'vue-router'
import NavigationMenu from '../NavigationMenu..vue'
const mockPush = vi.fn()
vi.mock('vue-router', () => ({
useRouter: vi.fn(),
}))
describe('NavigationMenu with mocked router', () => {
it('should handle navigation with mocked router', async () => {
const mockRouter = {
push: mockPush,
currentRoute: { value: { path: '/' } },
} as unknown as Router
vi.mocked(useRouter).mockImplementation(() => mockRouter)
const user = userEvent.setup()
render(NavigationMenu)
await user.click(screen.getByText('Profile'))
expect(mockPush).toHaveBeenCalledWith('/profile')
})
})
用于隔离测试的 RouterLink Stub
创建一个 RouterLink Stub来测试没有 RouterLink 行为的导航
import { Component, h } from 'vue'
import { useRouter } from 'vue-router'
export const RouterLinkStub: Component = {
name: 'RouterLinkStub',
props: {
to: {
type: [String, Object],
required: true,
},
tag: {
type: String,
default: 'a',
},
exact: Boolean,
exactPath: Boolean,
append: Boolean,
replace: Boolean,
activeClass: String,
exactActiveClass: String,
exactPathActiveClass: String,
event: {
type: [String, Array],
default: 'click',
},
},
setup(props) {
const router = useRouter()
const navigate = () => {
router.push(props.to)
}
return { navigate }
},
render() {
return h(
this.tag,
{
onClick: () => this.navigate(),
},
this.$slots.default?.(),
)
},
}
在测试中使用 RouterLinkStub:
import type { Router } from 'vue-router';
import userEvent from '@testing-library/user-event'
import { render, screen } from '@testing-library/vue'
import { describe, expect, it, vi } from 'vitest'
import { useRouter } from 'vue-router'
import NavigationMenu from '../NavigationMenu..vue'
import { RouterLinkStub } from './test-utils'
const mockPush = vi.fn()
vi.mock('vue-router', () => ({
useRouter: vi.fn(),
}))
describe('NavigationMenu with mocked router', () => {
it('should handle navigation with mocked router', async () => {
const mockRouter = {
push: mockPush,
currentRoute: { value: { path: '/' } },
} as unknown as Router
vi.mocked(useRouter).mockImplementation(() => mockRouter)
const user = userEvent.setup()
render(NavigationMenu, {
global: {
stubs: {
RouterLink: RouterLinkStub,
},
},
})
await user.click(screen.getByText('Dashboard'))
expect(mockPush).toHaveBeenCalledWith('/dashboard')
})
})
测试导航守卫
通过在路由上下文渲染组件来测试导航守卫
<script setup lang="ts">
import { onBeforeRouteLeave } from 'vue-router'
onBeforeRouteLeave(() => {
return window.confirm('Do you really want to leave this page?')
})
</script>
<template>
<div>
<h1>Route Leave Guard Demo</h1>
<div>
<nav>
<router-link to="/">
Home
</router-link> |
<router-link to="/about">
About
</router-link> |
<router-link to="/guard-demo">
Guard Demo
</router-link>
</nav>
</div>
</div>
</template>
测试导航守卫
import userEvent from '@testing-library/user-event'
import { render, screen } from '@testing-library/vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createRouter, createWebHistory } from 'vue-router'
import RouteLeaveGuardDemo from '../RouteLeaveGuardDemo.vue'
const routes = [
{ path: '/', component: RouteLeaveGuardDemo },
{ path: '/about', component: { template: '<div>About</div>' } },
]
const router = createRouter({
history: createWebHistory(),
routes,
})
const App = { template: '<router-view />' }
describe('RouteLeaveGuardDemo', () => {
beforeEach(async () => {
vi.clearAllMocks()
window.confirm = vi.fn()
await router.push('/')
await router.isReady()
})
it('should prompt when guard is triggered and user confirms', async () => {
// Set window.confirm to simulate a user confirming the prompt
window.confirm = vi.fn(() => true)
// Render the component within a router context
render(App, {
global: {
plugins: [router],
},
})
const user = userEvent.setup()
// Find the 'About' link and simulate a user click
const aboutLink = screen.getByRole('link', { name: /About/i })
await user.click(aboutLink)
// Assert that the confirm dialog was shown with the correct message
expect(window.confirm).toHaveBeenCalledWith('Do you really want to leave this page?')
// Verify that the navigation was allowed and the route changed to '/about'
expect(router.currentRoute.value.path).toBe('/about')
})
})
可复用的路由测试帮助函数
创建一个辅助函数来简化路由设置
import type { RenderOptions } from '@testing-library/vue'
import { render } from '@testing-library/vue'
import { createRouter, createWebHistory } from 'vue-router'
// path of the definition of your routes
import { routes } from '../../router/index.ts'
interface RenderWithRouterOptions extends Omit<RenderOptions<any>, 'global'> {
initialRoute?: string
routerOptions?: {
routes?: typeof routes
history?: ReturnType<typeof createWebHistory>
}
}
export function renderWithRouter(Component: any, options: RenderWithRouterOptions = {}) {
const { initialRoute = '/', routerOptions = {}, ...renderOptions } = options
const router = createRouter({
history: createWebHistory(),
// Use provided routes or import from your router file
routes: routerOptions.routes || routes,
})
router.push(initialRoute)
return {
// Return everything from regular render, plus the router instance
...render(Component, {
global: {
plugins: [router],
},
...renderOptions,
}),
router,
}
}
在测试中使用助手:
describe('NavigationMenu', () => {
it('should navigate using router links', async () => {
const { router } = renderWithRouter(NavigationMenu, {
initialRoute: '/',
})
await router.isReady()
const user = userEvent.setup()
await user.click(screen.getByText('Dashboard'))
expect(router.currentRoute.value.path).toBe('/dashboard')
})
})
结论:Vue Router 组件测试的最佳实践
当我们测试依赖于路由的组件时,我们需要考虑是否要在最真实的用例中测试功能,还是单独测试。在我看来,模拟测试越多,测试结果就越糟糕。我个人的建议是尽量使用真正的路由,而不是模拟它。有时,也会有例外,所以请记住这一点。
此外,您可以通过专注于不依赖路由功能的组件来帮助自己。为视图/页面组件保留路由逻辑。在保持组件简单的同时,我们永远不会遇到模拟路由的问题。
评论区
评论加载中...