Vue Router 4 是 Vue 3 的官方路由库。自从引入了组合式 API,我们可以在新版的 Vue Router 中使用新的组合式函数。那么我们该如何来模拟这些函数呢?目前 Vue Test Utils 官网文档并没有这方面的介绍。因此,本教程将讲解如何使用 Vitest 模拟 Vue Router,以便测试依赖于应用程序导航的业务逻辑。
简而言之,我们需要用模拟来替换当前的实现,以支持新的组合式函数 useRouter
和 useRoute
。相比之下,对于全局对象 $route
和 $router
,我们可以在组件挂载时将它们作为全局变量提供。
让我们通过为示例应用程序开发小的测试用例来看看如何实现这一点。
先决条件
确保你已经使用 npm init vue@latest
安装了一个带有 vitest 和 vue-router 的 Vue 3 应用,如下图所示:
在 Vite 配置中将其测试环境设置为:
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['src/setupTests.ts']
}
设置globals
为true
,不用导入describe
、test
、expect
、beforeEach
和afterEach
;指定setupFiles
后,省去导入@testing-library/jest-dom
。
此外,你还应该安装了 jsdom、testing-library/vue 和 testing-library/jest-dom
npm i -D @testing-library/jest-dom @testing-library/vue jsdom
你也可以使用 Vue Test Utils,当然和 Vue Testing Library 的 操作断言语法是不同的
示例应用程序
假设我们有两个示例页面组件和路由:
Index.vue
将渲染根路由并显示一个按钮,点击按钮可以编程方式跳转到问候页面。Hello.vue
将渲染/greeting
路由,并接受一个名字的查询参数来进行问候。
Index
组件将如下所示:
<!-- // src/pages/Index.vue -->
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { RouteNames } from '../router/router'
const router = useRouter()
console.log()
function onClick() {
router.push({
name: RouteNames.HELLO,
query: {
name: 'Xiao ming'
}
})
}
</script>
<template>
<button @click="onClick">Go to Hello</button>
</template>
Hello
组件将如下所示:
<!-- // src/pages/Hello.vue -->
<script lang="ts" setup>
import { RouterLink, useRoute } from 'vue-router'
import { RouteNames } from '../router/router'
const route = useRoute()
</script>
<template>
<h1>Good morning {{ route.query.name }}</h1>
<RouterLink :to="{ name: RouteNames.INDEX }">Go Back Home</RouterLink>
</template>
路由文件将包含路由名称,并将每个路由映射到相应的组件。然后,导出一个新的 Router 实例:
// src/router/router.ts
import { createRouter, createWebHistory } from 'vue-router'
export const RouteNames = {
INDEX: 'index',
HELLO: 'hello',
HOME: 'home',
GREETING: 'greeting',
}
const routes = [
{
path: '/',
name: RouteNames.INDEX,
component: () => import('../pages/Index.vue'),
},
{
path: '/hello',
name: RouteNames.HELLO,
component: () => import('../pages/Hello.vue'),
},
{
path: '/home',
name: RouteNames.HOME,
component: () => import('../pages/Home.vue'),
},
{
path: '/greeting',
name: RouteNames.GREETING,
component: () => import('../pages/Greeting.vue'),
},
]
export const router = createRouter({
routes,
history: createWebHistory(),
})
适当地使用导出的 RouteNames
常量,方便代码维护。
在 main.ts
中,我们导入路由并在 Vue 应用程序中使用它:
// src/main.tsx
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')
最后,在 App.vue
文件中,我们使用全局组件 <RouterView />
来渲染与当前路由对应的组件:
<!-- // src/App.vue -->
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
<template>
<div>
<RouterView />
</div>
</template>
当我们运行应用程序时,应该能够进行前进和后退的导航。现在让我们为这两个组件编写单元测试。
如何模拟 useRouter
我们的第一个测试将针对 Index
组件,我们在其中使用 useRouter
来导航到 /greeting
。由于我们使用了组合式 API,我们必须模拟 useRouter
方法。该方法返回我们之前导出并注册的路由对象的实例。
首先,创建规范文件并指定测试用例:
// src/pages/Index.spec.ts
import { render, fireEvent } from '@testing-library/vue'
import { vi } from 'vitest'
import { useRouter } from 'vue-router'
import { RouteNames } from '../router/router'
import Index from './Index.vue'
import type { Mock } from 'vitest'
vi.mock('vue-router')
const createRouter = useRouter as Mock
describe('Index', () => {
test(`navigates to ${RouteNames.GREETING} route and
sets query parameter name equal to ${NAME}`, async () => {
const { getByRole } = render(Index)
await fireEvent.click(getByRole('button', { name: /Go to Hello/i }))
})
})
接下来,测试路由的 push
方法是否以正确的参数进行调用。我们并不真正关心路由和浏览器导航 API 之间的交互,这已经在库本身内部进行了测试。因此,导入所需的方法并模拟 vue-router
包。然后模拟 useRouter
函数:
expect(useRouter().push).toHaveBeenCalledWith({
name: RouteNames.HELLO,
query: {
name: NAME,
}
})
正如你所见,我们将其模拟为返回一个模拟的 push
方法。现在我们可以对这个模拟对象进行多种操作。例如,我们可以检查它是否以某些参数进行了调用:
createRouter.mockReturnValue({
push: vi.fn()
})
请注意,我们在 beforeEach
块内使用了 mockReset
。这是因为我们希望在执行任何新的测试用例之前清除调用历史记录。如果我们多次使用 push
,保留调用历史记录将引入错误的结果。
最后,运行测试,你应该看到它通过了。
如何模拟 useRoute
在 Hello
组件中,我们使用 useRoute
来提取 URL 中的查询参数,并显示问候消息,让我们为该组件编写测试套件。
首先,和之前一样,创建规范文件并写下测试应该期望的结果:
// src/pages/Hello.spec.ts
import { render } from '@testing-library/vue'
import { vi, type Mock } from 'vitest'
import { useRoute } from 'vue-router'
import { RouteNames } from '../router/router'
import Hello from './Hello.vue'
vi.mock('vue-router')
const createRoute = useRoute as Mock
describe(RouteNames.HELLO, () => {
const name = 'Xiao ming'
test('renders greeting message for the URL query param "name"', () => {
const { getByRole } = render(Hello)
})
})
接下来,模拟 useRoute
函数并提供名称作为查询参数:
createRoute.mockReturnValue({
query: {
name
}
})
最后,使用 getByRole
来选择它 h1 ,并断言问候消息是否包含该名称:
expect(getByRole('heading', { level: 1 }))
.toHaveTextContent(`Good morning ${name}`)
还有一件事,你可能会收到一个警告,提示 RouterLink 未被识别为 Vue 组件,这是因为我们在模拟 vue-router
库,为了解决这个问题,将一个空函数组件添加到组件选项中:
const { getByRole } = render(Hello, {
global: {
stubs: {
RouterLink: () => null
}
}
})
最后,测试一下是否看是否通过。
模拟 $router 和 $route
首先,将 Index
组件调整为使用 $router
而不是 useRouter
:
// src/pages/Home.vue
this.$router.push({
name: RouteNames.GREETING,
query: {
name: 'Lao li'
}
})
接下来,调整测试用例以模拟全局对象,创建一个模拟对象并将其添加到 globals.mock
:
// src/pages/Home.spec.ts
// ...
const $routerMock = {
push: vi.fn(),
}
// ...
test(`navigates to ${RouteNames.HOME} route
and sets query parameter name equal to ${NAME}`, async () => {
const { getByRole } = render(Home, {
global: {
mocks: {
$router: $routerMock,
}
}
})
// ...
})
对于 Hello
组件,同样调整它以使用 $route
:
<!-- // src/pages/Greeting.vue -->
<h1>Good morning {{ $route.query.name }}</h1>
然后,使用一个模拟对象调整测试用例:
// src/pages/Greeting.spec.ts
// ...
const $routeMock = {
query: {
name: NAME
},
}
test('renders greeting message for the URL query param "name"', () => {
const { getByRole } = render(Greeting, {
global: {
stubs: ['RouterLink'],
mocks: {
$route: $routeMock,
},
}
})
// ...
})
最后,检查测试是否仍然通过。
总结
关于怎样模拟 $router
和 $route
,Vue Test Utils 官网有更详细的讲述,本文重点是讲解 useRouter
和 useRoute
这两个组合式函数的模拟测试。
链接
- 测试 Vue Router
- 访问 codesandbox 查看项目代码
评论 (0)