Vue Test Utils 组件测试
Vue 组件测试 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
掌握 Vue Test Utils 的进阶能力:全局配置与插件注入(Pinia、Router)、插槽与子组件断言、异步状态(loading → 数据到达)、以及 setProps 驱动的更新测试。
问题场景
组件依赖 Pinia store 和 Router,直接 mount 报错「no active Pinia」或「缺少 router 实例」。另一个组件在 onMounted 里请求数据,测试里断言时数据还没回来,wrapper.text() 是空的。这两个问题对应两条关键 API:全局注入与异步等待。
核心解释
注入插件:mount(Component, { global: { plugins: [pinia, router] } }):
import { createPinia, setActivePinia } from "pinia";
import { createRouter, createMemoryHistory } from "vue-router";
import { mount } from "@vue/test-utils";
import CartBadge from "../src/components/CartBadge.vue";
const router = createRouter({
history: createMemoryHistory(),
routes: [{ path: "/", component: { template: "<div />" } }],
});
const pinia = createPinia();
setActivePinia(pinia);
const wrapper = mount(CartBadge, {
global: { plugins: [pinia, router] },
});
createMemoryHistory 是 Router 的测试专用 history:没有真实 URL,但路由状态完整可用。测试里要断言「跳转到了 /login」,用 router.currentRoute.value.path,或者 await router.isReady() 后检查。
注入 provide:V09 的依赖注入测试时直接提供假值,比如 mount(MenuItem, { global: { provide: { [themeKey]: { theme: ref("dark"), toggleTheme: vi.fn() } } } })——不必渲染整棵祖先树。
异步组件与数据:用 flushPromises 等所有微任务清空,用 vi.waitFor 等条件成立:
import { flushPromises, mount } from "@vue/test-utils";
it("加载完成后渲染课程标题", async () => {
vi.stubGlobal("fetch", vi.fn(async () => ({
ok: true,
json: async () => ({ title: "Vue 3 全景" }),
})));
const wrapper = mount(CourseCard, { props: { slug: "v01" } });
expect(wrapper.text()).toContain("加载中");
await flushPromises();
expect(wrapper.text()).toContain("Vue 3 全景");
expect(wrapper.text()).not.toContain("加载中");
});
flushPromises 清空微任务队列;如果组件里有定时器(防抖、轮询),用假定时器配合 advanceTimersByTimeAsync。
插槽与 props 更新:验证作用域插槽把数据交给父模板,以及 props 变化后的渲染。断言子组件契约时,用 wrapper.findComponent(Child) 拿实例,再用 props() 与 emitted() 检查;不要深入子组件的 DOM 结构,否则子组件改一次样式就会连累父组件的测试。
it("作用域插槽把 item 传给父模板", () => {
const wrapper = mount(DataList, {
props: { items: [{ id: 1, title: "Vue" }] },
slots: {
default: `<template #default="{ item }"><span class="row">{{ item.title }}</span></template>`,
},
});
expect(wrapper.get(".row").text()).toBe("Vue");
});
it("error prop 变化时显示错误提示", async () => {
const wrapper = mount(TextField, { props: { modelValue: "", label: "邮箱", error: "" } });
expect(wrapper.find(".error").exists()).toBe(false);
await wrapper.setProps({ error: "邮箱格式不正确" });
expect(wrapper.get(".error").text()).toContain("邮箱格式不正确");
});
示例拆解
一个带 Pinia 的购物车角标测试,覆盖「空购物车」与「加入商品后」:
import { describe, expect, it, beforeEach } from "vitest";
import { createPinia, setActivePinia } from "pinia";
import { mount } from "@vue/test-utils";
import CartBadge from "../src/components/CartBadge.vue";
import { useCartStore } from "../src/stores/cart";
describe("CartBadge", () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it("空购物车显示 0 件", () => {
const wrapper = mount(CartBadge);
expect(wrapper.text()).toContain("0 件");
});
it("加入商品后角标更新", async () => {
const cart = useCartStore();
const wrapper = mount(CartBadge);
cart.add({ id: 1, name: "键盘", price: 399 });
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain("1 件");
expect(wrapper.text()).toContain("399");
});
});
关键:每个测试前重建 Pinia(setActivePinia(createPinia())),避免状态在测试间泄漏;直接操作 store 而不是模拟点击,测的是「组件对状态的反应」这一层契约。
动手任务
- 给一个依赖 store 的组件写测试,验证「空状态 → 加数据 → 更新」。
- 给一个依赖 Router 的页面组件写测试,注入
createMemoryHistory并断言当前路由。 - 用
setProps给一个表单组件写「错误提示出现/消失」的测试。
常见错误
- Pinia 状态跨测试泄漏:不重建 Pinia,第二个测试看到第一个的残留。
beforeEach里setActivePinia(createPinia())。 - 忘了 await flushPromises:异步数据断言在请求完成前执行,必然失败。
- 用真实 router(history 模式):测试环境没有真实 URL,用
createMemoryHistory。 - 断言子组件内部 DOM:耦合实现。断言子组件收到的 props 或触发的事件(
findComponent+props()/emitted())。 - 忘了给异步组件留等待:数据没到就断言必然失败;
flushPromises清微任务,vi.waitFor等条件,两者按场景选,别用固定setTimeout赌时序。
检查与复盘
- 能给组件注入 Pinia、Router、provide 三类依赖
- 能区分 flushPromises、waitFor、nextTick 的使用场景
- 能用 setProps 与 slots 测试组件的动态行为
参考资料
- Vue Test Utils 官方文档:global 配置与异步
- Pinia 官方文档「测试」:setActivePinia 的用法
读完这一课了?
标记完成后,工作台会安排接下来的学习。