claude-skills 实战:用 Vitest 与 Vue Test Utils 为 Vue 3 组件构建完整测试体系(vue-expert-js 测试模式指南)
2026/9/16 14:57:13 网站建设 项目流程

claude-skills 实战:用 Vitest 与 Vue Test Utils 为 Vue 3 组件构建完整测试体系(vue-expert-js 测试模式指南)

【免费下载链接】claude-skills67 Specialized Skills for Full-Stack Developers. Transform Claude Code into your expert pair programmer.项目地址: https://gitcode.com/GitHub_Trending/claud/claude-skills

导读

本指南以 claude-skills 仓库中 vue-expert-js 技能的 testing-patterns 参考文档 为核心骨架,面向使用纯 JavaScript(无 TypeScript)构建 Vue 3 应用的开发者,系统讲解基于 Vitest 与 Vue Test Utils 的组件测试完整方案。读完本文,你将掌握测试环境初始化、组件渲染与事件断言、v-model 双向绑定验证、异步请求处理、Composable 与 Pinia 状态 Mock、provide/inject 依赖注入测试等全套实战技能,并了解这些模式在 claude-skills 技能库中的定位与搭配方式。

该技能是 claude-skills 仓库 67 个全栈开发技能之一,定位为"纯 JavaScript + JSDoc 类型标注的 Vue 专家",其 SKILL.md 明确要求使用 Composition API 的<script setup>语法(不带lang="ts"),禁止.ts扩展名,用@typedef@param@returns等 JSDoc 注释实现完整的类型覆盖。在这种"JS 优先"的技术栈下,测试环节同样以纯 JS 的 Vitest 测试文件为主。


一、测试环境搭建:vitest.config.js 与全局 Setup

1.1 Vitest 配置:jsdom 环境与全局 API

组件测试需要一个模拟浏览器 DOM 的运行环境。Vue 官方测试工具链中,Vitest 负责运行与断言,Vue Test Utils 负责挂载组件,jsdom 负责提供 DOM API。以下是该技能推荐的 vitest.config.js 配置:

// vitest.config.js import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], test: { environment: 'jsdom', globals: true, setupFiles: ['./vitest.setup.js'] } })

各配置项说明:

配置项作用说明
plugins: [vue()]集成@vitejs/plugin-vue让 Vitest 复用 Vite 对.vue单文件组件的编译能力,这是组件测试的前提
environment: 'jsdom'指定测试环境jsdom 在 Node 中模拟浏览器 DOM;若测试不涉及 DOM 可改用node以提升速度
globals: true启用全局 APIdescribeitexpect等无需 import 即可使用,测试代码更简洁
setupFiles: ['./vitest.setup.js']测试启动前执行的准备文件集中放置全局 Mock 与通用配置

注:即使启用了globals,原文档的测试示例仍显式 import 了describe, it, expect,这在团队代码中更利于 IDE 类型提示(配合该技能的 JSDoc 类型约定),两种写法均可行。

1.2 全局 Setup:Mock fetch 与通用组件 Stub

vitest.setup.js 负责在测试启动时注册全局 Mock:

// vitest.setup.js import { config } from '@vue/test-utils' import { vi } from 'vitest' vi.stubGlobal('fetch', vi.fn()) config.global.stubs = { 'router-link': { template: '<a><slot /></a>' } }

逐行拆解:

  • vi.stubGlobal('fetch', vi.fn()):把全局fetch替换为可编程的 Mock 函数。Vue 3 应用中大量组件依赖fetch发起请求(参考 state-management.md 中loginaction 对/api/login的调用),在 jsdom 环境中该 API 默认不可用,统一打桩后即可在单个测试中按需 mock 返回值。
  • config.global.stubs:Vue Test Utils 的全局配置。凡是模板里出现<router-link>的地方,一律替换为一个渲染<a><slot /></a>的轻量桩组件。这样被测组件不再依赖 Vue Router 实例,渲染出的锚点文本仍可被wrapper.text()断言。同理可扩展 stubtransitionteleport等内置组件。

这套"环境全局打桩 + 组件全局 stub"的写法,让每个测试文件只需关注自身逻辑,无需重复处理基础设施。


二、组件测试基础:渲染、插槽、类名、事件与属性

组件测试的核心循环是"挂载 → 交互 → 断言"。原文档以 Button 组件为例,覆盖了 Vue 组件四大基础断言面:

// Button.test.js import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import Button from './Button.vue' describe('Button', () => { it('renders slot content', () => { const wrapper = mount(Button, { slots: { default: 'Click me' } }) expect(wrapper.text()).toBe('Click me') }) it('applies variant class', () => { const wrapper = mount(Button, { props: { variant: 'danger' } }) expect(wrapper.classes()).toContain('btn--danger') }) it('emits click event', async () => { const wrapper = mount(Button) await wrapper.trigger('click') expect(wrapper.emitted('click')).toHaveLength(1) }) it('is disabled when prop is true', () => { const wrapper = mount(Button, { props: { disabled: true } }) expect(wrapper.attributes('disabled')).toBeDefined() }) })

四个断言技巧逐一说明:

  1. 插槽渲染mount(Button, { slots: { default: 'Click me' } })注入默认插槽内容,wrapper.text()返回组件渲染的完整文本。断言插槽是否被正确渲染与透传。
  2. props 驱动的类名:传入variant: 'danger',断言组件根据 variant 动态拼接的 CSS 类。若 Button 内部按:class="\btn--${variant}`"` 生成类名,测试即验证了"输入 prop → 输出样式"的契约。
  3. 事件触发trigger('click')模拟点击,wrapper.emitted('click')返回该事件所有发射记录的数组。toHaveLength(1)验证事件恰好触发一次。注意触发事件通常需要await,等待 DOM 更新与事件队列刷新。
  4. 属性断言wrapper.attributes('disabled')检查渲染出的 HTML 是否带disabled属性。

这段示例与 SKILL.md 中 JSDoc 化 props 定义形成闭环:技能要求组件用@typedef+defineProps声明 props(如isAdmin布尔默认值、variant校验器等),而测试正是对这些声明契约的逐项验证。


三、v-model 双向绑定测试:验证 update:modelValue 事件

v-model 本质上是modelValueprop 与update:modelValue事件的语法糖。测试 v-model 的正确姿势是:模拟用户输入,断言update:modelValue事件携带了正确的载荷。原文档的 TextInput 示例:

// TextInput.test.js import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import TextInput from './TextInput.vue' describe('TextInput', () => { it('emits update:modelValue on input', async () => { const wrapper = mount(TextInput, { props: { modelValue: '' } }) await wrapper.find('input').setValue('new value') expect(wrapper.emitted('update:modelValue')).toEqual([['new value']]) }) })

要点:

  • setValue('new value')会设置 input 的 value 并触发input事件,是模拟用户输入的标准方式(也可用于selecttextarea)。
  • emitted('update:modelValue')返回二维数组,外层每项对应一次事件发射,内层是该次发射的载荷列表,因此toEqual([['new value']])断言"恰好发射一次,载荷为字符串'new value'"。
  • 组件侧的实现可参考 component-architecture.md 中的 v-model 模式:defineProps({ modelValue })+defineEmits(['update:modelValue']),模板中:value="modelValue" @input="emit('update:modelValue', $event.target.value)"。该文档还给出了多 v-model(v-model:firstNameupdate:firstName)的扩展写法,对应测试只需断言emitted('update:firstName')即可。

四、异步测试:flushPromises 与 fetch Mock 组合

Vue 组件的onMounted中常发异步请求。测试这类组件必须等待 Promise 队列清空,否则断言发生在渲染之前。Vue Test Utils 提供了flushPromises()工具,原文档的 UserList 示例是标准范式:

// UserList.test.js import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import UserList from './UserList.vue' describe('UserList', () => { beforeEach(() => vi.resetAllMocks()) it('renders users after fetch', async () => { global.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve([{ id: 1, name: 'Alice' }]) }) const wrapper = mount(UserList) await flushPromises() expect(wrapper.text()).toContain('Alice') }) it('shows error on failure', async () => { global.fetch = vi.fn().mockRejectedValue(new Error('Network error')) const wrapper = mount(UserList) await flushPromises() expect(wrapper.find('[data-test="error"]').exists()).toBe(true) }) })

要点拆解:

  1. beforeEach(() => vi.resetAllMocks()):每个测试前重置所有 mock(包括 setup 文件里全局打桩的fetch),避免测试间状态泄漏。因为 setup 中已用vi.stubGlobal('fetch', vi.fn())打桩,此处直接改写global.fetch的 mock 实现即可。
  2. 成功路径mockResolvedValue返回一个模拟 Response 对象,其json()也返回 Promise,构造完整的链式调用;flushPromises()等待 fetch 的 resolve 与组件内部的渲染队列全部完成,然后断言文本包含 'Alice'。
  3. 失败路径mockRejectedValue(new Error(...))模拟网络故障,断言组件渲染了data-test="error"的错误提示元素。这里体现了data-test属性作为查询锚点的约定——比依赖 CSS 类名更稳定,与原文档 Quick Reference 中wrapper.find('[data-test="x"]')的推荐一致。

从底层原理看,flushPromises()本质是清空当前微任务队列:Vue 组件的异步渲染(如nextTick回调)、Promise 链与watchEffect的触发都依赖微任务调度,await flushPromises()保证这些任务全部执行完毕。


五、Mocking Composables:vi.spyOn 替换组合式函数

Composable 是 Vue 3 Composition API 的逻辑复用单元,也是 JS 版 Vue 项目的核心组织方式(参考 composables-patterns.md,其中useToggleuseAsyncStateuseCancellableFetch等均以"返回{ ref, 方法 }对象"为统一结构)。测试依赖 Composable 的组件时,若不想真实执行内部副作用(如登录请求、定时器),可用vi.spyOn整体替换。原文档 Header 示例:

// Header.test.js import { describe, it, expect, vi } from 'vitest' import { mount } from '@vue/test-utils' import { ref, computed } from 'vue' import Header from './Header.vue' import * as useAuthModule from '@/composables/useAuth' describe('Header', () => { it('shows login button when logged out', () => { vi.spyOn(useAuthModule, 'useAuth').mockReturnValue({ user: ref(null), isLoggedIn: computed(() => false), login: vi.fn(), logout: vi.fn() }) const wrapper = mount(Header) expect(wrapper.find('[data-test="login-btn"]').exists()).toBe(true) }) it('shows user menu when logged in', () => { vi.spyOn(useAuthModule, 'useAuth').mockReturnValue({ user: ref({ id: 1, name: 'John' }), isLoggedIn: computed(() => true), login: vi.fn(), logout: vi.fn() }) const wrapper = mount(Header) expect(wrapper.find('[data-test="user-menu"]').exists()).toBe(true) }) })

关键点:

  • import * as useAuthModule from '@/composables/useAuth':把模块整体导入为命名空间对象,才能对其属性进行vi.spyOn(ESM 命名导出的标准 Mock 方式)。
  • Mock 返回值必须保持"响应式形状"user必须是ref()包装的响应式值,isLoggedIn必须是computed()。因为组件模板在渲染时会解包这些 ref/computed,如果直接返回普通对象,组件内部读取.value将得到undefined,测试就会失真。这也印证了该技能"Composable 返回{ ref, computed, 函数 }结构"的约定——Mock 只是按相同形状伪造数据。
  • 行为分支全覆盖:分别 mock 为"未登录"与"已登录"两种状态,断言[data-test="login-btn"][data-test="user-menu"]的条件渲染,验证组件的状态切换逻辑。

六、Pinia 状态测试:createTestingPinia 提供隔离 Store

现代 Vue 3 应用的状态管理以 Pinia 为主(参考 state-management.md,其中同时演示了 Options 语法与 Setup 语法定义 store、storeToRefs解构、store 间组合调用等模式)。测试依赖 store 的组件时,@pinia/testing提供的createTestingPinia能创建"默认所有 action 均为 vi.fn()、state 可用 initialState 预设"的隔离实例。原文档 CartSummary 示例:

// CartSummary.test.js import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import { createTestingPinia } from '@pinia/testing' import CartSummary from './CartSummary.vue' import { useCartStore } from '@/stores/cart' describe('CartSummary', () => { it('displays cart total', () => { const wrapper = mount(CartSummary, { global: { plugins: [createTestingPinia({ initialState: { cart: { items: [{ productId: 1, quantity: 2, price: 100 }] } } })] } }) expect(wrapper.text()).toContain('$200') }) it('calls checkout action', async () => { const wrapper = mount(CartSummary, { global: { plugins: [createTestingPinia()] } }) await wrapper.find('[data-test="checkout-btn"]').trigger('click') expect(useCartStore().checkout).toHaveBeenCalled() }) })

要点:

  1. 注入方式:Pinia 插件通过global.plugins数组传入,组件内部的useCartStore()会自动找到这个测试实例,无需手动setActivePinia
  2. initialState预设 state:键为 store id(cart),值为该 store 的 state 快照。组件渲染时从 store 读取items,计算总额2 × $100 = $200,断言文本。
  3. action 自动 MockcreateTestingPinia()默认把所有 actions 替换为vi.fn(),因此useCartStore().checkout在测试中不会真正发起网络请求,只需断言点击结算按钮后该 action 被调用。这正是上一节 state-management 文档中checkout()调用/api/checkout的实现逻辑——测试时用测试 Pinia 隔离掉真实副作用。
  4. 配合 store 单元测试:如果想单独测试 store 本身(不经组件),state-management.md 给出了另一套范式:beforeEach(() => setActivePinia(createPinia()))后用useCounterStore()直接断言increment()与 getter 结果。两者组合即可覆盖"store 逻辑层 + 组件渲染层"。

七、provide/inject 测试:global.provide 注入依赖

组件间依赖注入(provide/inject)用于跨层级传递数据。测试注入方组件时,Vue Test Utils 允许在挂载配置中直接通过global.provide提供注入值,无需真正渲染 Provider 祖先。原文档示例:

// ChildComponent.test.js import { describe, it, expect } from 'vitest' import { mount } from '@vue/test-utils' import { ref } from 'vue' import ChildComponent from './ChildComponent.vue' describe('ChildComponent', () => { it('uses injected theme', () => { const wrapper = mount(ChildComponent, { global: { provide: { theme: ref('dark') } } }) expect(wrapper.classes()).toContain('theme-dark') }) })
  • global.provide的对象键可以是字符串(与组件内inject('theme', ...)对应),也可以是 Symbol。
  • 注入值同样需要保持响应式形状:这里themeref('dark')包装,与组件内const theme = inject('theme')后在模板中解包使用的方式匹配。
  • 该模式与 component-architecture.md 中provideTheme/useTheme的 Symbol 组合式模式互补:若组件通过useTheme()消费注入,测试需用相同的ThemeSymbol作为provide的键;若注入缺失,useTheme()会抛出'useTheme requires ThemeProvider'错误,测试正好可以覆盖"未包裹 Provider 时给出明确报错"的边界场景。

八、测试骨架速查表

原文档末尾提供了一张高频操作速查表,覆盖组件测试最常用的 API 组合,在此完整保留并补充说明:

任务代码
挂载组件mount(Component, { props, slots, global })
查找元素wrapper.find('[data-test="x"]')
触发事件await wrapper.trigger('click')
检查事件wrapper.emitted('event')
设置输入await wrapper.find('input').setValue('x')
等待异步await flushPromises()
Mock Composablevi.spyOn(module, 'fn').mockReturnValue()
Mock fetchglobal.fetch = vi.fn().mockResolvedValue()
测试 PiniacreateTestingPinia({ initialState })
注入依赖global: { provide: { key: value } }
桩替换组件global: { stubs: { Comp: true } }

补充使用语境:

  • flushPromises:仅在组件内部存在异步逻辑(fetch、定时器、nextTick后的异步渲染)时必须使用;纯同步组件可直接断言。
  • stubs: { Comp: true }true表示用空组件替换;也可传入{ template: '...' }提供定制桩模板,与 setup 文件里对router-link的全局 stub 思路一致。
  • global配置是贯穿全篇的关键挂载选项,props/slots/global三者为mount最常用的三个配置维度。

九、在 claude-skills 技能体系中的定位与工作流衔接

本测试模式不是孤立的知识点,而是 vue-expert-js 技能完整工作流的最后一环。查看该技能 SKILL.md 的 Core Workflow:

  1. Design architecture:规划组件结构与带 JSDoc 类型标注的 Composable;
  2. Implement:用<script setup>(不带lang="ts")与.mjs模块实现;
  3. Annotate:为所有公共 API 补齐@typedef@param@returns,并用 ESLint 的 JSDoc 插件校验覆盖度;
  4. Test使用 JavaScript 文件通过 Vitest 验证,确认所有公共 API 的 JSDoc 覆盖,测试失败则回到对应组件或 Composable 修正逻辑或注解,直到测试套件全绿。

这意味着:

  • 测试文件与源码同为纯 JS:不引入.ts测试文件,遵守技能 MUST NOT DO 中"不使用 TypeScript 语法"的约束;
  • 类型与测试互为佐证:JSDoc 声明的 props 形状、Composable 返回结构、store state 结构,正是测试中 mock 数据与断言的对象;
  • 参考文档按需加载:SKILL.md 的 Reference Guide 表格将本测试模式文档标记为"Testing 主题(Vitest、组件测试、Mocking)时加载",与 jsdoc-typing.md、composables-patterns.md、component-architecture.md、state-management.md 并列。

此外,claude-skills 仓库还通过 scripts/validate-skills.py 对每个技能的 SKILL.md 进行结构校验(包括 Core Workflow 步骤数、引用文件路径可解析性、YAML frontmatter 完整性等),而 Makefile 中的validate目标会串联运行这些校验脚本。技能文档体系的健康度本身就有自动化保障,这间接保证了本文引用的模式与技能约定始终保持一致。


十、常见误区与最佳实践小结

避免的典型错误

  1. 忽略awaittriggersetValueflushPromises返回 Promise,不await会导致断言过早执行,得到空 emitted 或未渲染的 DOM。
  2. Mock 形状失真:mock 返回的 ref/computed 必须是真实的响应式包装,否则组件内.value解包失败;同理 store 的 initialState 键名必须与 store id 严格一致。
  3. 忘记隔离vi.resetAllMocks()(或vi.clearAllMocks())未在beforeEach调用时,前一个用例的 fetch 实现会泄漏到后一个用例,产生偶发失败。
  4. 查询锚点脆弱:优先使用data-test属性定位元素,而非依赖易变的 CSS 类名或深层 DOM 结构。

推荐实践

  • 三层测试分层:Composable(直接调用 + 断言返回值)→ Store(setActivePinia(createPinia())单元测试)→ 组件(mount+ 交互 + emitted/文本断言),三层各有独立范式,按需取舍而非全部照搬。
  • 全局 stub 集中管理router-linktransition等通用桩放在vitest.setup.jsconfig.global.stubs中,单测中只为特殊场景定制桩。
  • 始终围绕组件契约写断言:props 传入 → 渲染结果、事件触发 → emitted 载荷、注入依赖 → 派生样式/行为,测试即是对 JSDoc 声明的可执行验证。

延伸阅读

  • testing-patterns.md:本文核心文档原文
  • component-architecture.md:props/emits/v-model/provide-inject 组件契约的完整定义
  • state-management.md:Pinia store 定义与 store 单元测试范式
  • composables-patterns.md:Composable 结构约定,Mock 时需保持相同返回形状
  • jsdoc-typing.md:@typedef/@param/@returns完整类型标注规则
  • SKILL.md:vue-expert-js 技能总览与 Core Workflow

【免费下载链接】claude-skills67 Specialized Skills for Full-Stack Developers. Transform Claude Code into your expert pair programmer.项目地址: https://gitcode.com/GitHub_Trending/claud/claude-skills

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询