使用Vue Test Utils和Vitest时,单元测试$routerMock.push断言失败
问题描述
测试TheFooter.vue组件的路由跳转功能时,点击链接后触发AssertionError,错误信息:
AssertionError: expected "spy" to be called with arguments: []expected "spy" to be called with arguments:
组件代码(TheFooter.vue)
<template> <footer class="footer"> <ul class="footer-links"> <li v-for="link in footerLinks" :key="link" > <RouterLink data-test="footer-link" class="fw500-ar font-dark-L40-D50 footer-link" :to="{ name: link.link }" > {{ link.title }} </RouterLink> </li> </ul> </footer> </template> <script setup lang="ts"> import { computed } from "vue"; import { useI18n } from "vue-i18n"; const { tm } = useI18n({ useScope: "global" }); const footerLinks = computed(() => { return tm("footerLinks"); }); </script>
原测试代码(TheFooter.spec.ts)
import { flushPromises, shallowMount } from "@vue/test-utils"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { i18n } from "@/plugins/localization/localization"; import TheFooter from "@/components/layout/TheFooter.vue"; vi.mock("vue-router"); describe("TheFooter.vue unit test", () => { const $routerMock = { push: vi.fn(), }; let wrapper = shallowMount(TheFooter, { global: { plugins: [i18n], stubs: ["RouterLink"], mocks: { $router: $routerMock, }, }, }); beforeEach(async () => { $routerMock.push.mockReset(); wrapper = shallowMount(TheFooter, { global: { plugins: [i18n], stubs: ["RouterLink"], mocks: { $router: $routerMock, }, }, }); }); afterEach(() => { wrapper.unmount(); }); it("navigates to the correct route when a link is clicked", async () => { // Access all links using the data-test attribute const links = wrapper.find('[data-test="footer-link"]'); console.log("links", links.exists()); // Access the keys of the footerLinks object // const linkKeys = Object.keys(wrapper.vm.footerLinks); // Assert that the correct number of links is rendered // expect(links).toHaveLength(linkKeys.length); await flushPromises(); // Simulate a click on the first link await links.trigger("click"); // expect that navigation happened expect($routerMock.push).toHaveBeenCalledWith(); }); });
解决步骤
1. 自定义RouterLink Stub,模拟点击触发路由跳转
默认的RouterLink stub不会关联$router.push,需要自定义stub处理点击事件:
const RouterLinkStub = { props: ["to"], template: '<a @click="$router.push(to)" data-test="footer-link"><slot /></a>' };
2. Mock i18n的tm方法,提供测试用的footerLinks数据
避免依赖真实国际化配置,直接返回稳定的测试数据:
vi.spyOn(i18n.global, 'tm').mockReturnValue([ { title: 'Home', link: 'home' }, { title: 'About', link: 'about' } ]);
3. 修正元素获取与断言逻辑
- 用
findAll替代find获取所有链接 - 断言时传入明确的预期路由参数,而非空值
修改后的完整测试代码
import { flushPromises, shallowMount } from "@vue/test-utils"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { i18n } from "@/plugins/localization/localization"; import TheFooter from "@/components/layout/TheFooter.vue"; vi.mock("vue-router"); // 自定义RouterLink stub,模拟点击触发路由跳转 const RouterLinkStub = { props: ["to"], template: '<a @click="$router.push(to)" data-test="footer-link"><slot /></a>' }; describe("TheFooter.vue unit test", () => { const $routerMock = { push: vi.fn(), }; let wrapper; beforeEach(async () => { $routerMock.push.mockReset(); // Mock i18n的tm方法,返回测试用的链接数据 vi.spyOn(i18n.global, 'tm').mockReturnValue([ { title: 'Home', link: 'home' }, { title: 'About', link: 'about' } ]); wrapper = shallowMount(TheFooter, { global: { plugins: [i18n], stubs: { RouterLink: RouterLinkStub }, // 使用自定义stub mocks: { $router: $routerMock, }, }, }); await flushPromises(); }); afterEach(() => { wrapper.unmount(); vi.restoreAllMocks(); // 恢复mock }); it("renders correct number of footer links", () => { const links = wrapper.findAll('[data-test="footer-link"]'); expect(links).toHaveLength(2); }); it("navigates to home route when first link is clicked", async () => { const firstLink = wrapper.findAll('[data-test="footer-link"]')[0]; await firstLink.trigger("click"); expect($routerMock.push).toHaveBeenCalledWith({ name: 'home' }); }); it("navigates to about route when second link is clicked", async () => { const secondLink = wrapper.findAll('[data-test="footer-link"]')[1]; await secondLink.trigger("click"); expect($routerMock.push).toHaveBeenCalledWith({ name: 'about' }); }); });
关键修改说明
- 自定义RouterLink Stub:让点击事件直接调用
$router.push,模拟真实路由跳转行为 - Mock i18n数据:确保测试环境有稳定的footerLinks数据,不受外部配置影响
- 精确断言:每个测试用例明确断言对应路由参数,避免模糊的空参数断言
- 元素获取修正:用
findAll获取所有链接,支持多个链接的单独测试
内容的提问来源于stack exchange,提问作者Sahar Elbehery
相关产品推荐
相关产品推荐

