# Vue 测试 Vue 3 组件、组合式函数和工具函数的测试模式。 ## 快速参考 | 测试类型 | 模式 | | ---------------- | ------------------------------------- | | 组件 | `mount(Component, { props, slots })` | | 用户交互 | `await wrapper.trigger('click')` | | 触发的事件 | `wrapper.emitted('update')` | | 组合式函数 | 直接调用,测试返回值 | | 工具函数 | 纯函数测试(最简单) | ## 技术栈 - **Vitest** — 测试运行器 - **@vue/test-utils** — 组件挂载、交互 - **@testing-library/vue** — 以用户为中心的替代方案 - **happy-dom / jsdom** — DOM 环境 ## 文件位置 将测试文件与代码放在一起: ``` Button.vue → Button.spec.ts useAuth.ts → useAuth.spec.ts formatters.ts → formatters.spec.ts ``` ## 组件测试 ### 基本用法 ```ts import { mount } from '@vue/test-utils' import Button from './Button.vue' it('渲染插槽', () => { const wrapper = mount(Button, { slots: { default: '点击我' } }) expect(wrapper.text()).toBe('点击我') }) it('点击时触发事件', async () => { const wrapper = mount(Button) await wrapper.trigger('click') expect(wrapper.emitted('click')).toHaveLength(1) }) ``` ### Props ```ts it('应用 variant 类名', () => { const wrapper = mount(Button, { props: { variant: 'primary' } }) expect(wrapper.classes()).toContain('btn-primary') }) ``` ### 事件触发 ```ts it('触发 update 事件并携带载荷', async () => { const wrapper = mount(Input) await wrapper.find('input').setValue('新值') expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['新值']) }) ``` ### 插槽 ```ts it('渲染具名插槽', () => { const wrapper = mount(Card, { slots: { header: '

标题

', default: '

内容

' } }) expect(wrapper.html()).toContain('

标题

') }) ``` ## 组合式函数测试 直接调用,无需挂载: ```ts import { useCounter } from './useCounter' it('递增计数', () => { const { count, increment } = useCounter(0) expect(count.value).toBe(0) increment() expect(count.value).toBe(1) }) it('重置为初始值', () => { const { count, increment, reset } = useCounter(5) increment() increment() expect(count.value).toBe(7) reset() expect(count.value).toBe(5) }) ``` ## 工具函数测试 最简单——纯函数: ```ts import { formatCurrency, slugify } from './formatters' describe('formatCurrency', () => { it('格式化美元金额', () => { expect(formatCurrency(10.5)).toBe('$10.50') }) }) describe('slugify', () => { it('转换为小写', () => { expect(slugify('Hello World')).toBe('hello-world') }) it('移除特殊字符', () => { expect(slugify('Hello! World?')).toBe('hello-world') }) }) ``` ## Mock **组合式函数:** ```ts import { vi } from 'vitest' vi.mock('./useAuth', () => ({ useAuth: vi.fn(() => ({ user: { id: 1, name: 'Test' }, isAuthenticated: true })) })) ``` **API 调用:** ```ts global.fetch = vi.fn(() => Promise.resolve({ json: () => Promise.resolve({ data: [] }) }) ) ``` ## Router Mock 在组件测试中 Mock `useRoute` 和 `useRouter`: ```ts import { vi } from 'vitest' import { mount } from '@vue/test-utils' vi.mock('vue-router', () => ({ useRoute: vi.fn(() => ({ params: { id: '123' }, query: { filter: 'active' }, path: '/users/123', })), useRouter: vi.fn(() => ({ push: vi.fn(), replace: vi.fn(), })), })) it('使用路由参数', () => { const wrapper = mount(UserPage) expect(wrapper.text()).toContain('123') }) ``` **每个测试的动态路由 Mock:** ```ts import { useRoute } from 'vue-router' it('处理不同的路由', () => { vi.mocked(useRoute).mockReturnValue({ params: { id: '456' }, } as any) const wrapper = mount(UserPage) expect(wrapper.text()).toContain('456') }) ``` ## Suspense 与 Teleport **使用 Suspense 测试异步组件:** ```ts import { flushPromises, mount } from '@vue/test-utils' it('渲染异步内容', async () => { const wrapper = mount(AsyncComponent, { global: { stubs: { Suspense: false }, // 不对 Suspense 进行桩替换 }, }) // 等待异步 setup 完成 await flushPromises() expect(wrapper.text()).toContain('已加载内容') }) ``` **测试 Teleport:** ```ts it('传送模态框内容', () => { const wrapper = mount(Modal, { global: { stubs: { teleport: true, // 将 teleport 替换为桩,使其内联渲染 }, }, }) expect(wrapper.text()).toContain('模态框内容') }) ``` **访问被传送的内容:** ```ts it('查找被传送的内容', () => { document.body.innerHTML = '' mount(Modal, { props: { open: true } }) // 内容被传送到 #modal-target expect(document.body.innerHTML).toContain('模态框内容') }) ``` ## 最佳实践 **应做:** - 测试行为(用户看到/操作的内容),而非实现细节 - 采用 Arrange-Act-Assert(准备—执行—断言)结构 - 每个测试只做一个断言 - 使用描述性的测试名称 - Mock 外部依赖 **不应做:** - 测试 Vue 内部机制(响应式系统) - 测试第三方库 - 测试琐碎的 getter/setter - 测试实现细节 ## 测试内容 **需要测试:** - 用户交互(点击、输入) - 条件渲染 - Props 验证、触发的事件 - 计算属性、业务逻辑 **跳过:** - Vue 内部机制、第三方库 - 琐碎的 getter/setter - 实现细节 ## 运行 ```bash pnpm test # 全部 pnpm exec vitest Button.spec.ts # 指定文件 pnpm exec vitest --watch # 监听模式 pnpm test --coverage # 覆盖率 ``` **文档:** [vitest.dev](https://vitest.dev/) · [test-utils.vuejs.org](https://test-utils.vuejs.org/)