Vitest 基础与测试思路
Vue 组件测试 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
先建立测试的判断力:什么该测、什么不该测、测试金字塔在 Vue 项目里怎么落。然后跑通 Vitest 的最小配置,写出第一个组件测试。
问题场景
团队要求「覆盖率 80%」,于是大家给每个 getter、每个工具函数、每个组件都写测试。结果是 600 个测试、跑 8 分钟,重构一次 UI 全红,而真正会出错的交互流程(表单提交、列表筛选、权限跳转)反而没测。测试的价值不在数量,在「能拦住什么回归」。
核心解释
测什么(按价值排序):
- 用户可见的行为:渲染结果、点击后的变化、表单提交、路由跳转。
- 业务规则:价格计算、权限判断、校验逻辑(纯函数,最好测)。
- 边界与错误:空数据、加载失败、超长输入。
不该重点测:内部实现细节(用了几个 ref、调没调 watch)、纯样式的快照(改个 class 就红)、第三方库的行为(那不是你的代码)。
测试金字塔:大量纯函数测试(快、稳)→ 适量组件测试(Vitest + Test Utils)→ 少量端到端测试(Playwright,覆盖主流程)。Vue 官方建议把「组件测试」作为主要投入,因为它能覆盖「渲染 + 交互 + 状态」的组合。
环境搭建:npm i -D vitest @vue/test-utils jsdom @vitejs/plugin-vue,然后在 vite.config.js 里加上 vue 插件与 test 配置(environment: "jsdom"、globals: true),并在 package.json 里加 "test": "vitest run" 与 "test:watch": "vitest"。
environment: "jsdom" 提供 DOM;globals: true 让 describe/it/expect 无需 import(也可以显式 import,更利于 IDE 提示)。
第一个组件测试:
// tests/Counter.spec.js
import { describe, expect, it } from "vitest";
import { mount } from "@vue/test-utils";
import Counter from "../src/components/Counter.vue";
describe("Counter", () => {
it("初始显示 0", () => {
const wrapper = mount(Counter);
expect(wrapper.text()).toContain("计数:0");
});
it("点击按钮后计数加一", async () => {
const wrapper = mount(Counter);
await wrapper.get("button").trigger("click");
expect(wrapper.text()).toContain("计数:1");
});
});
三个 API 要记:mount 渲染组件返回 wrapper;wrapper.get(selector) 找元素,trigger 派发事件;await trigger 让 Vue 完成更新(内部等 nextTick),漏了 await 会读到旧 DOM。
示例拆解
给一个「待办项」组件写第一组测试,覆盖渲染与交互:
<!-- src/components/TodoItem.vue -->
<script setup>
const props = defineProps({ todo: { type: Object, required: true } });
const emit = defineEmits(["toggle", "remove"]);
</script>
<template>
<li>
<input type="checkbox" :checked="props.todo.done" @change="emit('toggle', props.todo.id)" />
<span>{{ props.todo.title }}</span>
<button @click="emit('remove', props.todo.id)">删除</button>
</li>
</template>
// tests/TodoItem.spec.js
import { mount } from "@vue/test-utils";
import TodoItem from "../src/components/TodoItem.vue";
const todo = { id: 1, title: "读 V16", done: false };
it("渲染标题与勾选状态", () => {
const wrapper = mount(TodoItem, { props: { todo } });
expect(wrapper.text()).toContain("读 V16");
expect(wrapper.get("input[type=checkbox]").element.checked).toBe(false);
});
it("勾选时发出 toggle 并携带 id", async () => {
const wrapper = mount(TodoItem, { props: { todo } });
await wrapper.get("input[type=checkbox]").trigger("change");
expect(wrapper.emitted("toggle")).toBeTruthy();
expect(wrapper.emitted("toggle")[0]).toEqual([1]);
});
it("点击删除发出 remove 事件", async () => {
const wrapper = mount(TodoItem, { props: { todo } });
await wrapper.get("button").trigger("click");
expect(wrapper.emitted("remove")[0]).toEqual([1]);
});
wrapper.emitted("事件名") 返回每次触发的事件参数数组,是测子组件契约的核心工具。注意测试里用 props: { todo } 传数据,不 mock 组件内部状态。
动手任务
- 搭好 Vitest 环境,给一个计数器组件写「初始值 / 点击 +1 / 重置」三个测试。
- 给 TodoItem 的三个测试跑通,故意把 emit 的参数从 id 改成整个对象,观察测试如何失败。
- 给一个纯函数(价格计算)写测试,体会纯函数测试比组件测试快多少、稳多少。
常见错误
- trigger 不 await:DOM 还没更新就断言,测试偶发失败。所有交互后都 await。
- 测试里直接用真实网络:慢且不稳定。fetch 一律 mock(V13)。
- 用
wrapper.find找不到就返回空:断言exists()或直接用get(找不到会抛错,更容易定位)。 - 测了样式快照:改 padding 就红,维护成本高收益低。
检查与复盘
- 能说出测试金字塔在 Vue 项目里的落地方式
- 能写出 mount + trigger + emitted 的基本测试
- 能判断一个断言是在测行为还是测实现
参考资料
- Vue 官方文档「测试」:测试类型与工具选择
- Vue Test Utils 官方文档:mount 与 trigger
- Vitest 官方文档:环境与配置
读完这一课了?
标记完成后,工作台会安排接下来的学习。