原文地址:How to Test Vue Router Components with Testing Library and Vitest
作者是Alex,德国程序员,写了很多优质的文章,专注于Vue,AI和GraphQL等前沿技术。你可以点击下面的链接访问他的博客,获取更多优质内容。
ヾ(•ω•`)o
让我们开始这次精彩的阅读吧!介绍
现代 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 组件测试的最佳实践
当我们测试依赖于路由的组件时,我们需要考虑是否要在最真实的用例中测试功能,还是单独测试。在我看来,模拟测试越多,测试结果就越糟糕。我个人的建议是尽量使用真正的路由,而不是模拟它。有时,也会有例外,所以请记住这一点。
此外,您可以通过专注于不依赖路由功能的组件来帮助自己。为视图/页面组件保留路由逻辑。在保持组件简单的同时,我们永远不会遇到模拟路由的问题。
评论区
评论加载中...