测试 Vue Router
本文将介绍两种使用 Vue Router 测试应用程序的方法
- 使用真实的 Vue Router,这更像生产环境,但在测试大型应用程序时也可能导致复杂性
- 使用模拟路由器,允许更细粒度地控制测试环境。
请注意,Vue 测试工具没有提供任何特殊功能来帮助测试依赖于 Vue Router 的组件。
使用模拟路由器
您可以使用模拟路由器来避免在单元测试中关心 Vue Router 的实现细节。
我们可以创建一个模拟版本,它只实现了我们感兴趣的功能,而不是使用真实的 Vue Router 实例。我们可以使用 jest.mock
(如果您使用的是 Jest)和 global.components
的组合来做到这一点。
当我们模拟一个依赖项时,通常是因为我们对测试它的行为不感兴趣。我们不想测试点击 <router-link>
是否导航到正确的页面 - 当然可以!不过,我们可能对确保 <a>
具有正确的 to
属性感兴趣。
让我们看一个更现实的例子!此组件显示一个按钮,该按钮将重定向经过身份验证的用户到编辑帖子页面(基于当前路由参数)。未经身份验证的用户应被重定向到 /404
路由。
const Component = {
template: `<button @click="redirect">Click to Edit</button>`,
props: ['isAuthenticated'],
methods: {
redirect() {
if (this.isAuthenticated) {
this.$router.push(`/posts/${this.$route.params.id}/edit`)
} else {
this.$router.push('/404')
}
}
}
}
我们可以使用真实的路由器,然后导航到此组件的正确路由,然后在单击按钮后断言渲染了正确的页面... 然而,对于一个相对简单的测试来说,这需要大量的设置。从本质上讲,我们要编写的测试是“如果经过身份验证,则重定向到 X,否则重定向到 Y”。让我们看看如何通过模拟使用 global.mocks
属性的路由来实现这一点
import { mount } from '@vue/test-utils';
test('allows authenticated user to edit a post', async () => {
const mockRoute = {
params: {
id: 1
}
}
const mockRouter = {
push: jest.fn()
}
const wrapper = mount(Component, {
props: {
isAuthenticated: true
},
global: {
mocks: {
$route: mockRoute,
$router: mockRouter
}
}
})
await wrapper.find('button').trigger('click')
expect(mockRouter.push).toHaveBeenCalledTimes(1)
expect(mockRouter.push).toHaveBeenCalledWith('/posts/1/edit')
})
test('redirect an unauthenticated user to 404', async () => {
const mockRoute = {
params: {
id: 1
}
}
const mockRouter = {
push: jest.fn()
}
const wrapper = mount(Component, {
props: {
isAuthenticated: false
},
global: {
mocks: {
$route: mockRoute,
$router: mockRouter
}
}
})
await wrapper.find('button').trigger('click')
expect(mockRouter.push).toHaveBeenCalledTimes(1)
expect(mockRouter.push).toHaveBeenCalledWith('/404')
})
我们使用 global.mocks
来提供必要的依赖项(this.$route
和 this.$router
)来为每个测试设置理想状态。
然后,我们能够使用 jest.fn()
来监控 this.$router.push
被调用了多少次以及使用哪些参数。最棒的是,我们不必在测试中处理 Vue Router 的复杂性或注意事项!我们只关心测试应用程序逻辑。
提示
您可能希望以端到端的方式测试整个系统。您可以考虑使用像 Cypress 这样的框架,使用真实的浏览器进行完整的系统测试。
使用真实的路由器
现在我们已经了解了如何使用模拟路由器,让我们来看看如何使用真实的 Vue Router。
让我们创建一个使用 Vue Router 的基本博客应用程序。帖子列在 /posts
路由上
const App = {
template: `
<router-link to="/posts">Go to posts</router-link>
<router-view />
`
}
const Posts = {
template: `
<h1>Posts</h1>
<ul>
<li v-for="post in posts" :key="post.id">
{{ post.name }}
</li>
</ul>
`,
data() {
return {
posts: [{ id: 1, name: 'Testing Vue Router' }]
}
}
}
应用程序的根目录显示一个 <router-link>
,它指向 /posts
,我们在那里列出帖子。
真实的路由器如下所示。请注意,我们将路由与路由分开导出,以便稍后为每个单独的测试实例化一个新的路由器。
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
component: {
template: 'Welcome to the blogging app'
}
},
{
path: '/posts',
component: Posts
}
];
const router = createRouter({
history: createWebHistory(),
routes: routes,
})
export { routes };
export default router;
说明如何使用 Vue Router 测试应用程序的最佳方法是让警告引导我们。以下最小测试足以让我们开始
import { mount } from '@vue/test-utils'
test('routing', () => {
const wrapper = mount(App)
expect(wrapper.html()).toContain('Welcome to the blogging app')
})
测试失败。它还打印了两个警告
console.warn node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:39
[Vue warn]: Failed to resolve component: router-link
console.warn node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:39
[Vue warn]: Failed to resolve component: router-view
<router-link>
和 <router-view>
组件未找到。我们需要安装 Vue Router!由于 Vue Router 是一个插件,因此我们使用 global.plugins
安装选项来安装它
import { mount } from '@vue/test-utils'
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from "@/router" // This import should point to your routes file declared above
const router = createRouter({
history: createWebHistory(),
routes: routes,
})
test('routing', () => {
const wrapper = mount(App, {
global: {
plugins: [router]
}
})
expect(wrapper.html()).toContain('Welcome to the blogging app')
})
这两个警告现在消失了 - 但现在我们又有了另一个警告
console.warn node_modules/vue-router/dist/vue-router.cjs.js:225
[Vue Router warn]: Unexpected error when starting the router: TypeError: Cannot read property '_history' of null
虽然从警告中并不完全清楚,但它与Vue Router 4 异步处理路由这一事实有关。
Vue Router 提供了一个 isReady
函数,它告诉我们路由器何时准备就绪。然后,我们可以 await
它以确保初始导航已发生。
import { mount } from '@vue/test-utils'
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from "@/router"
const router = createRouter({
history: createWebHistory(),
routes: routes,
})
test('routing', async () => {
router.push('/')
// After this line, router is ready
await router.isReady()
const wrapper = mount(App, {
global: {
plugins: [router]
}
})
expect(wrapper.html()).toContain('Welcome to the blogging app')
})
测试现在通过了!这需要相当多的工作,但现在我们确保应用程序正确地导航到初始路由。
现在让我们导航到 /posts
并确保路由按预期工作
import { mount } from '@vue/test-utils'
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from "@/router"
const router = createRouter({
history: createWebHistory(),
routes: routes,
})
test('routing', async () => {
router.push('/')
await router.isReady()
const wrapper = mount(App, {
global: {
plugins: [router]
}
})
expect(wrapper.html()).toContain('Welcome to the blogging app')
await wrapper.find('a').trigger('click')
expect(wrapper.html()).toContain('Testing Vue Router')
})
同样,另一个有点神秘的错误
console.warn node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:39
[Vue warn]: Unhandled error during execution of native event handler
at <RouterLink to="/posts" >
console.error node_modules/@vue/runtime-core/dist/runtime-core.cjs.js:211
TypeError: Cannot read property '_history' of null
同样,由于 Vue Router 4 的新异步特性,我们需要 await
路由完成,然后才能进行任何断言。
但是,在这种情况下,没有我们可以等待的 hasNavigated 钩子。一种替代方法是使用 Vue 测试工具导出的 flushPromises
函数
import { mount, flushPromises } from '@vue/test-utils'
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from "@/router"
const router = createRouter({
history: createWebHistory(),
routes: routes,
})
test('routing', async () => {
router.push('/')
await router.isReady()
const wrapper = mount(App, {
global: {
plugins: [router]
}
})
expect(wrapper.html()).toContain('Welcome to the blogging app')
await wrapper.find('a').trigger('click')
await flushPromises()
expect(wrapper.html()).toContain('Testing Vue Router')
})
它最终通过了。太棒了!然而,这一切都非常手动 - 而且这是针对一个微不足道的应用程序。这就是在使用 Vue 测试工具测试 Vue 组件时使用模拟路由器是一种常见方法的原因。如果您更喜欢继续使用真实的路由器,请记住,每个测试都应该使用它自己的路由器实例,如下所示
import { mount, flushPromises } from '@vue/test-utils'
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from "@/router"
let router;
beforeEach(async () => {
router = createRouter({
history: createWebHistory(),
routes: routes,
})
});
test('routing', async () => {
router.push('/')
await router.isReady()
const wrapper = mount(App, {
global: {
plugins: [router]
}
})
expect(wrapper.html()).toContain('Welcome to the blogging app')
await wrapper.find('a').trigger('click')
await flushPromises()
expect(wrapper.html()).toContain('Testing Vue Router')
})
使用 Composition API 的模拟路由器
Vue 路由器 4 允许使用 Composition API 在 setup
函数中使用路由器和路由。
考虑使用 Composition API 重写的相同演示组件。
import { useRouter, useRoute } from 'vue-router'
const Component = {
template: `<button @click="redirect">Click to Edit</button>`,
props: ['isAuthenticated'],
setup (props) {
const router = useRouter()
const route = useRoute()
const redirect = () => {
if (props.isAuthenticated) {
router.push(`/posts/${route.params.id}/edit`)
} else {
router.push('/404')
}
}
return {
redirect
}
}
}
这次,为了测试组件,我们将使用 jest 的能力来模拟导入的资源 vue-router
并直接模拟路由器和路由。
import { useRouter, useRoute } from 'vue-router'
jest.mock('vue-router', () => ({
useRoute: jest.fn(),
useRouter: jest.fn(() => ({
push: () => {}
}))
}))
test('allows authenticated user to edit a post', () => {
useRoute.mockImplementationOnce(() => ({
params: {
id: 1
}
}))
const push = jest.fn()
useRouter.mockImplementationOnce(() => ({
push
}))
const wrapper = mount(Component, {
props: {
isAuthenticated: true
},
global: {
stubs: ["router-link", "router-view"], // Stubs for router-link and router-view in case they're rendered in your template
}
})
await wrapper.find('button').trigger('click')
expect(push).toHaveBeenCalledTimes(1)
expect(push).toHaveBeenCalledWith('/posts/1/edit')
})
test('redirect an unauthenticated user to 404', () => {
useRoute.mockImplementationOnce(() => ({
params: {
id: 1
}
}))
const push = jest.fn()
useRouter.mockImplementationOnce(() => ({
push
}))
const wrapper = mount(Component, {
props: {
isAuthenticated: false
}
global: {
stubs: ["router-link", "router-view"], // Stubs for router-link and router-view in case they're rendered in your template
}
})
await wrapper.find('button').trigger('click')
expect(push).toHaveBeenCalledTimes(1)
expect(push).toHaveBeenCalledWith('/404')
})
使用 Composition API 的真实路由器
使用 Composition API 的真实路由器与使用 Options API 的真实路由器的工作方式相同。请记住,就像使用 Options API 一样,将每个测试实例化一个新的路由器对象,而不是直接从您的应用程序导入路由器,这被认为是一种良好的做法。
import { mount } from '@vue/test-utils'
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from "@/router"
let router;
beforeEach(async () => {
router = createRouter({
history: createWebHistory(),
routes: routes,
})
router.push('/')
await router.isReady()
});
test('allows authenticated user to edit a post', async () => {
const wrapper = mount(Component, {
props: {
isAuthenticated: true
},
global: {
plugins: [router],
}
})
const push = jest.spyOn(router, 'push')
await wrapper.find('button').trigger('click')
expect(push).toHaveBeenCalledTimes(1)
expect(push).toHaveBeenCalledWith('/posts/1/edit')
})
对于那些喜欢非手动方法的人来说,Posva 创建的库 vue-router-mock 也可用作替代方案。
结论
- 您可以在测试中使用真实的路由器实例。
- 不过,有一些注意事项:Vue Router 4 是异步的,我们在编写测试时需要考虑这一点。
- 对于更复杂的应用程序,请考虑模拟路由器依赖项并专注于测试底层逻辑。
- 尽可能利用测试运行器的存根/模拟功能。
- 使用
global.mocks
来模拟全局依赖项,例如this.$route
和this.$router
。