讲解
前端测试的金字塔:底部是大量单元测试(一个函数、一个 composable 的行为),中间是组件测试(渲染组件、模拟交互、断言输出),顶部是少量端到端测试(真实浏览器跑完整流程)。学习路径也按这个顺序:先把纯逻辑测明白,再碰组件渲染。
Vue 生态的标准装备:测试框架 Vitest(Vite 亲儿子,和项目共用配置,速度快),组件测试库 @vue/test-utils(mount 组件、触发事件、查找节点)。一个组件测试的骨架是三段式:Arrange(准备数据和挂载)、Act(触发交互)、Assert(断言 DOM 或状态)。测试 composable 更简单——它只是函数,直接调用、断言返回值,连 mount 都不用。
写测试的心法是「测行为,不测实现」:断言「点击后计数显示 2」,而不是「内部变量 count 等于 2」——内部重构不该让测试翻车。另一个心法是「一个用例一个断言点」:失败时一眼定位问题。mock 外部依赖(接口请求用 vi.fn() 或 MSW 拦截),让测试快且稳定——不能每次跑测试都真打后端。
示例
先测纯逻辑:用 Node.js 自带的断言库直接测 composable(这正是本教程所有章节自我验证的方式,以下代码真实执行):
import assert from 'node:assert/strict';
// 被测对象:上一章风格的 useCounter(纯函数,无需 Vue 运行时)
function useCounter(initial = 0, { min = -Infinity, max = Infinity } = {}) {
const count = { value: initial };
return {
count,
inc: () => {
if (count.value < max) count.value++;
},
dec: () => {
if (count.value > min) count.value--;
},
};
}
// 用例 1:初始值
{
const { count } = useCounter(5);
assert.strictEqual(count.value, 5);
}
// 用例 2:增减行为
{
const { count, inc, dec } = useCounter(0);
inc();
inc();
dec();
assert.strictEqual(count.value, 1);
}
// 用例 3:边界——到 max 后不再增长
{
const { count, inc } = useCounter(0, { max: 2 });
inc();
inc();
inc();
inc();
assert.strictEqual(count.value, 2);
}
// 用例 4:边界——min 兜底
{
const { count, dec } = useCounter(1, { min: 0 });
dec();
dec();
assert.strictEqual(count.value, 0);
}
console.log('4 个用例全部通过:初始值 / 增减 / max 边界 / min 边界');
真实项目里的组件测试(Vitest + @vue/test-utils):
// Counter.spec.js —— 仅示意:真实项目代码(依赖 vitest/@vue/test-utils),构建时不执行
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import Counter from './Counter.vue';
describe('Counter', () => {
it('初始显示传入的 start 值', () => {
const wrapper = mount(Counter, { props: { start: 5 } });
expect(wrapper.text()).toContain('5');
});
it('点击按钮后计数加一', async () => {
const wrapper = mount(Counter, { props: { start: 0 } });
await wrapper.find('button').trigger('click'); // 交互后要 await 等 DOM 更新
expect(wrapper.text()).toContain('1');
});
it('到达 max 后按钮禁用', async () => {
const wrapper = mount(Counter, { props: { start: 1, max: 2 } });
await wrapper.find('button').trigger('click');
expect(wrapper.find('button').attributes('disabled')).toBeDefined();
});
});
常见坑
- 交互后忘记 await:trigger('click') 后 DOM 更新是异步的,不 await 就断言拿到的是旧 DOM。
- 测实现细节:断言内部变量名、方法调用次数,重构一次全红;断言用户能感知的行为。
- 一个用例塞太多步骤:失败时不知道哪步错了,拆小用例、用 describe 分组。
- 不 mock 网络请求:测试慢、依赖后端在线、数据不稳定;vi.fn/vi.mock 或 MSW 拦掉。
- 只测快乐路径:边界(空列表、max 值、网络失败)才是 bug 高发区,先写边界用例。
小结
Vitest + @vue/test-utils;纯逻辑(composable)直接调用断言,组件测试三段式 Arrange/Act/Assert;测行为不测实现,边界优先。最后一章:把应用交付给用户——构建与部署。