You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Vue Test Utils和Vitest时,单元测试$routerMock.push断言失败

解决Vue Test Utils + Vitest测试Footer路由跳转的AssertionError问题

问题描述

测试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();
  });
});

解决步骤

默认的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 18:33:17