《Vue3 从入门到大神29篇》单元测试与 E2E 测试 —— 保障 Vue3 项目的质量
前言很多 Vue 项目都有一个共同点✅ 功能能跑❌ 一改就崩❌ 不敢重构❌ 上线提心吊胆问题不在于代码写得不好而在于缺少自动化测试。在 Vue3 生态中测试体系已经非常成熟测试类型工具作用单元测试Vitest测函数、Composable组件测试Vue Test Utils Vitest测组件行为E2E 测试Cypress测用户完整操作流程覆盖率c8 / Istanbul评估测试完整性这一篇我们从零搭建一个企业级 Vue3 测试方案。一、为什么需要测试1️⃣ 没有测试的代价 修复一个 Bug引入三个新 Bug 重构代码像拆炸弹 回归测试全靠人工点点点 上线前集体加班2️⃣ 测试金字塔/\ / \ E2E 测试少量 /----\ / \ 集成测试适量 /--------\ / \ 单元测试大量 /------------\原则单元测试快、多、便宜E2E 测试慢、少、贵越往上越接近真实用户二、VitestVue3 的官方测试伴侣1️⃣ 为什么选 Vitest对比项JestVitest速度较慢✅ 极快ESM 原生Vite 集成需配置✅ 开箱即用Vue 支持需插件✅ 原生支持热更新❌✅并发执行❌✅Vitest 是为 Vite 量身定制的测试框架。2️⃣ 安装与配置pnpm add -D vitest vue/test-utils jsdom testing-library/vue// vitest.config.ts import { defineConfig } from vitest/config import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], test: { environment: jsdom, globals: true, setupFiles: [./tests/setup.ts] } })// package.json { scripts: { test: vitest, test:coverage: vitest run --coverage } }3️⃣ 测试文件约定tests/ ├── unit/ # 单元测试 │ └── useCounter.test.ts ├── components/ # 组件测试 │ └── Counter.spec.vue ├── e2e/ # E2E 测试 │ └── auth.cy.ts └── setup.ts # 测试环境配置三、单元测试Composable 测试1️⃣ 测试 useCounter// composables/useCounter.ts import { ref } from vue export function useCounter(initial 0) { const count ref(initial) const inc () count.value const dec () count.value-- return { count, inc, dec } }// tests/unit/useCounter.test.ts import { describe, it, expect } from vitest import { useCounter } from /composables/useCounter describe(useCounter, () { it(should initialize with 0, () { const { count } useCounter() expect(count.value).toBe(0) }) it(should increment count, () { const { count, inc } useCounter() inc() expect(count.value).toBe(1) }) it(should decrement count, () { const { count, dec } useCounter(5) dec() expect(count.value).toBe(4) }) })2️⃣ 测试异步逻辑// composables/useFetch.ts import { ref } from vue export function useFetchT(url: string) { const data refT | null(null) const loading ref(false) const fetchData async () { loading.value true const res await fetch(url) data.value await res.json() loading.value false } return { data, loading, fetchData } }// tests/unit/useFetch.test.ts import { describe, it, expect, vi } from vitest import { useFetch } from /composables/useFetch // Mock fetch global.fetch vi.fn() describe(useFetch, () { it(should fetch data successfully, async () { const mockData { id: 1, name: Tom } ;(fetch as any).mockResolvedValueOnce({ json: async () mockData }) const { data, loading, fetchData } useFetch(/api/user) await fetchData() expect(loading.value).toBe(false) expect(data.value).toEqual(mockData) }) })四、组件测试Vue Test Utils1️⃣ 测试一个计数器组件!-- components/Counter.vue -- template div button clickdec-/button span{{ count }}/span button clickinc/button /div /template script setup langts import { useCounter } from /composables/useCounter const { count, inc, dec } useCounter() /script// tests/components/Counter.spec.ts import { describe, it, expect } from vitest import { mount } from vue/test-utils import Counter from /components/Counter.vue describe(Counter.vue, () { it(renders initial count, () { const wrapper mount(Counter) expect(wrapper.text()).toContain(0) }) it(increments when plus button clicked, async () { const wrapper mount(Counter) await wrapper.findAll(button)[1].trigger(click) expect(wrapper.text()).toContain(1) }) it(decrements when minus button clicked, async () { const wrapper mount(Counter) await wrapper.findAll(button)[0].trigger(click) expect(wrapper.text()).toContain(-1) }) })2️⃣ 测试 Props 和 Emits!-- components/UserCard.vue -- template div clickhandleClick {{ user.name }} /div /template script setup langts defineProps{ user: { name: string } }() const emit defineEmits{ (e: select, name: string): void }() const handleClick () emit(select, Tom) /script// tests/components/UserCard.spec.ts import { mount } from vue/test-utils import UserCard from /components/UserCard.vue it(emits select event when clicked, async () { const wrapper mount(UserCard, { props: { user: { name: Tom } } }) await wrapper.trigger(click) expect(wrapper.emitted(select)?.[0]).toEqual([Tom]) })五、Pinia 测试1️⃣ 测试 Store// stores/user.ts import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ user: null as { name: string } | null }), actions: { setUser(user: { name: string }) { this.user user } } })// tests/stores/user.test.ts import { setActivePinia, createPinia } from pinia import { useUserStore } from /stores/user describe(User Store, () { beforeEach(() { setActivePinia(createPinia()) }) it(sets user correctly, () { const store useUserStore() store.setUser({ name: Tom }) expect(store.user?.name).toBe(Tom) }) })2️⃣ 组件中使用 Store 的测试// tests/components/UserProfile.spec.ts import { mount } from vue/test-utils import { createPinia } from pinia import UserProfile from /components/UserProfile.vue import { useUserStore } from /stores/user it(displays user name from store, () { const pinia createPinia() const wrapper mount(UserProfile, { global: { plugins: [pinia] } }) const store useUserStore() store.setUser({ name: Tom }) expect(wrapper.text()).toContain(Tom) })六、E2E 测试Cypress1️⃣ 安装与配置pnpm add -D cypress// package.json { scripts: { cy:open: cypress open, cy:run: cypress run } }2️⃣ 登录流程测试// cypress/e2e/auth.cy.ts describe(Authentication, () { it(should login successfully, () { cy.visit(/login) cy.get([data-testidusername]).type(admin) cy.get([data-testidpassword]).type(123456) cy.get([data-testidsubmit]).click() cy.url().should(include, /dashboard) cy.contains(Welcome, admin).should(be.visible) }) it(should show error on invalid credentials, () { cy.visit(/login) cy.get([data-testidsubmit]).click() cy.contains(用户名或密码错误).should(be.visible) }) })3️⃣ 测试数据属性推荐 ✅input data-testidusername / button data-testidsubmit /比 class / id 更稳定七、测试覆盖率1️⃣ 生成覆盖率报告pnpm test:coverage2️⃣ 覆盖率配置// vitest.config.ts export default defineConfig({ test: { coverage: { reporter: [text, json, html], exclude: [ node_modules/, tests/, **/*.spec.ts, **/*.test.ts ] } } })八、测试最佳实践实践说明AAA 模式Arrange准备→ Act执行→ Assert断言测试行为不是实现关注输入输出不关注内部逻辑避免测试私有方法通过公有 API 间接测试使用 data-testid提高选择器稳定性测试隔离每个测试独立不依赖其他测试模拟外部依赖API、定时器、WebSocket九、常见误区误区真相测试覆盖率 100%成本高收益递减测试越多越好维护成本会上升E2E 测试替代单元测试两者互补不可替代测试拖慢开发长期来看节省调试时间十、面试高频问答Q1单元测试和 E2E 测试的区别单元测试测函数/组件E2E 测完整用户流程。Q2为什么 Vue3 推荐 Vitest因为 Vite 原生支持速度更快配置更简单。Q3如何测试 Vue 组件使用 Vue Test Utils 挂载组件模拟用户交互断言结果。十一、总结工程级测试是项目质量的护城河Vitest 是 Vue3 单元测试的首选组件测试关注行为和交互E2E 测试保障核心业务流程测试不是为了找 Bug而是为了防止 Bug 下期预告第 30 篇Vue3 生态全景图及未来趋势SSR/Nuxt

相关新闻