资讯动态

Vue 测试实践:从 Vitest 到 Playwright 的完整搭建与用例设计

发布时间:2026/9/9 19:21:19 来源:尧图企业网站定制
从写组件到不敢重构中间只差一个测试体系。第 57 天我在 Vue 进阶路线里停在了“测试”这一关花了一整天把单元测试和环境配置摸了一遍又用端到端测试跑通了完整的登录流程。这篇文章是那天折腾完之后的完整记录包含工具选型思路、环境搭建步骤、组件测试的常用 API 实战以及一个登录表单从单元测试到端到端测试的完整案例最后附上我在实际项目中踩过的坑。1. 第 57 天的回头审视Vue 进阶路上为什么绕不开测试先说一个场景如果你已经能熟练地用 Vue 写组件、配路由、管状态那你大概率也经历过这种时刻——一个看似很小的改动比如把某个 props 的默认值改了一下结果所有引用它的页面突然都不对了。排查一圈发现是某个子组件内部把 props 处理成了另一种结构。这种问题如果靠手动点页面去发现效率极低而且很容易漏。我在学习路线走到第 57 天的时候前面已经完成了组件化开发、Composition API、Vue Router、Pinia 这些内容单独拎出来这些知识都能写但放在一个真实的项目里心里的底气是不够的。原因很简单没有一套自动化的手段告诉我“这次改动有没有把别的地方弄坏”。测试解决的就是这个安全感问题。它不是在项目写完之后补作业式的点缀而是给代码库装上一张安全网。有了它重构才敢下手升级依赖才敢点确认新同事接手代码也不至于全靠猜。测试分很多层但前端领域最常提、也最该先掌握的就是单元测试和端到端测试这两类。单元测试Unit Test针对的是代码中的最小单元——在 Vue 项目里这个单元通常就是一个组件、一个 composable、一个工具函数。它运行在 Node 环境里通过模拟浏览器环境jsdom/happy-dom来渲染组件验证组件在不同输入下的行为。它的特点是快几百个用例几秒钟跑完适合在开发阶段持续回归。端到端测试E2E Test则完全不同。它启动一个真实浏览器从用户的角度点击、输入、跳转验证整个应用在真实运行时的表现。它的特点是慢但覆盖面广能验证页面跳转、网络请求、真实交互这一整条链路是否正常。这两者不是二选一的关系而是配合使用的关系。单元测试负责把每一块砖头测扎实端到端测试负责验证整面墙不会倒。这篇博文按照我当天的实操顺序来写从工具选型讲到环境配置再讲到具体用例最后是一堆真实踩坑记录。如果你也正处于 Vue 进阶阶段准备系统接触测试这篇应该能帮你少走一些弯路。2. 工具链确定的思路Vitest、Vue Test Utils、Playwright 的组合逻辑这年头做 Vue 测试工具已经不是问题问题是怎么选。我先说结论再展开讲理由单元测试/组件测试Vitest Vue Test Utils jsdom端到端测试Playwright环境Vite Vue 3组合式 API 项目包管理器npm 即可这个组合不是拍脑袋定的而是分别比较过几个备选项之后拿到的结果。下面说清楚每个选择的理由。2.1 为什么单元测试选 Vitest 而不是 JestJest 是前几年 Vue 官方文档里推荐的测试框架生态成熟文档也多。但如果你用的是 Vite 构建的项目Jest 和 Vite 之间其实存在一层适配成本。Jest 有自己独立的模块解析和转换流程要在 Vite 项目里跑 Jest需要额外装 jest-transform-vue 或者手动配置 transformer遇到 ESM 依赖多的时候还会撞上各种模块解析问题。Vitest 是 Vite 团队自己出的测试框架底层直接复用 Vite 的配置和依赖预打包机制。这意味着项目里已有的 vite.config.ts 可以直接被测试环境继承不需要单独维护一份测试专用配置。另外一个很实在的优势是速度Vitest 利用 Vite 的模块图做依赖预分析配合 watch 模式下的按需执行跑起来几乎是秒开。我在一个中等规模项目上对比过Vitest 首次冷启动大概 2 到 3 秒Jest 往往要 7 到 8 秒起步。对于测试这种需要频繁执行来获得反馈的工作这个差距会直接影响使用意愿。还有一点是 TypeScript 支持。Vitest 本身就能直接解析 TS 文件测试文件里写类型标注、泛型都不需要额外封装而 Jest 配合 ts-jest 有时会因为 tsconfig 路径别名配置不一致导致测试报找不到模块。2.2 Vue Test Utils组件测试的官方答案Vue Test UtilsVTU是 Vue.js 官方团队维护的组件测试库可以把它理解成一套“操作和断言 Vue 组件的工具方法”。它提供了 mount、shallowMount、trigger、props、emitted 等 API让你能够在测试里模拟渲染组件、触发事件、读取传参和输出。这里要特别说明一点Vitest 负责的是测试运行和断言它只管“跑起来”和“比较结果”但“怎么把 Vue 组件渲染出来、怎么模拟用户操作”这件事需要 Vue Test Utils 来完成。两者是配合关系谁也替代不了谁。类似的方案还有 vue/test-utils 配合 Jest 使用但既然 Vitest 已经定了VTU 自然就成了首选因为 Vue 官方就是按这个组合来维护的遇到问题时更容易查到针对性资料。2.3 端到端测试选 Playwright 而不是 CypressE2E 测试工具里比较主流的有 Cypress 和 Playwright我选了 Playwright原因是它在几个关键点上更适合 Vue 项目。首先是安装体验差很多。Cypress 需要下载一个自带的 Electron 运行时在国内网络环境下经常卡在下载那一步。Playwright 虽然也要下载浏览器但提供镜像环境变量整体流程更顺。其次是语言和写法。Playwright 使用标准的 async/await 写法配合 Pytest 和 Jest 风格断言Vue 开发者上手几乎没成本Cypress 的链式 API 有它自己的理解成本。最关键的是 Playwright 内置了“自动等待”机制——你调用 click、type 这些动作时它会自动等待元素可见、可交互不用在测试代码里到处写 sleep这比手动等待稳定太多了。网页自动生成测试代码功能也是 Playwright 的一个涨停理由用测试模式打开浏览器手动操作一遍页面就能自动化生成对应测试代码对初学者尤其是好消息。2.4 工具链版本选型的总结工具作用选型理由Vitest测试运行器 断言与 Vite 同构、速度快、TS 原生支持Vue Test Utils组件挂载与交互模拟Vue 官方维护配合 Vitest 无需额外配置jsdom浏览器环境模拟在 Node 里模拟 DOM API让组件可渲染Playwright端到端浏览器测试安装稳、自动等待、有代码生成器组合确定之后接下来的任务就是把这些工具真正跑起来。3. 从零搭建测试环境让第一个测试用例真正跑起来测试环境搭建是整个过程中最容易卡住新手的一环。很多教程会直接给配置代码但对每个配置项的作用解释不够导致复制粘贴之后报错也不知从哪查起。我在这里把搭建过程按步骤拆开写每一步做什么、为什么这么改尽量说清楚。3.1 初始化项目或指定已有项目如果你是从零开始先用 Vite 创建一个 Vue 3 项目npm create vitelatest vue-test-demo -- --template vue-ts cd vue-test-demo npm install这里我特意用了 vue-ts 模板因为 TypeScript 在测试里提供类型提示报错信息也更可读。如果已经有项目就直接在项目根目录操作即可。3.2 安装测试相关依赖npm install -D vitest vue/test-utils jsdom vitejs/plugin-vue解释一下每个包具体干什么vitest 是测试框架vue/test-utils 是挂载组件和模拟交互的库jsdom 是让 Node 环境模拟出 DOM 行为的运行环境vitejs/plugin-vue 在这个项目已经依赖了这里再确认一遍因为 Vitest 需要它来处理 .vue 单文件组件。注意不需要安装 vue/vite-plugin-vue 或 vue-jest那是 Jest 路线才需要的东西。3.3 配置 vite.config.ts在已有的 vite.config.ts 里新增 test 字段/// reference typesvitest / import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], test: { environment: jsdom, globals: true, }, })上面有两个细节值得单独解释。第一行的/// reference typesvitest /是 TypeScript 的三斜线指令它的作用是让 TS 识别 test 字段和 describe/it/expect 这些全局变量。如果不加这行编辑器通常会报类型错误。另一种做法是在 tsconfig.json 里加上types: [vitest/globals]两种都可以但我习惯用三斜线指令因为它更直观不会影响全局 tsconfig 的配置。environment: jsdom表示测试在 jsdom 提供的模拟 DOM 中运行。如果不配置Vitest 默认是 node 环境没有 document、window挂载组件会直接报错。globals: true的意思是让 describe、it、expect 这些测试 API 变成全局变量不需要在每个测试文件里 import。用起来更简洁。如果你不喜欢隐式依赖可以不开启然后手动import { describe, it, expect, vi } from vitest两种写法人人可接受但注意不要混用。3.4 写第一个测试用例新建一个测试文件夹src/__tests__然后写一个极简组件src/components/HelloWorld.vuetemplate div p{{ message }}/p button clickupdateMessage更新文案/button /div /template script setup langts import { ref } from vue const props defineProps{ message: string }() const currentMessage ref(props.message) const updateMessage () { currentMessage.value 点击后的文案 } /script对应的测试文件import { describe, it, expect } from vitest import { mount } from vue/test-utils import HelloWorld from ../components/HelloWorld.vue describe(HelloWorld, () { it(渲染传入的 message props, () { const wrapper mount(HelloWorld, { props: { message: 你好Vue 测试 }, }) expect(wrapper.text()).toContain(你好Vue 测试) }) it(点击按钮后更新文案, async () { const wrapper mount(HelloWorld, { props: { message: 初始文案 }, }) await wrapper.find(button).trigger(click) expect(wrapper.text()).toContain(点击后的文案) }) })package.json 里增加脚本scripts: { test: vitest }命令行运行npm test你会看到 Vitest 进入 watch 模式并输出测试通过的结果。能跑到这一步环境就基本通了。3.5 环境配置里最容易出问题的细节跑通第一个用例之后建议把几个配置项提前了解清楚否则后面写复杂用例时会突然报错test.include默认会匹配**/*.{test,spec}.?(c|m)[jt]s?(x)等模式所以测试文件命名为.test.ts或.spec.ts都可以不用改配置。如果项目里用了作为 src 的路径别名需要在 vite.config.ts 里配置resolve.alias测试会自动继承不需要在测试配置里重复写。如果用了css: true或未开启 CSS 处理测试组件可能会因为引入样式文件而告警一般不影响结果但建议在测试环境关闭 CSS不加载 CSS因为测试不关心样式。4. 组件测试的三个核心动作挂载、交互、断言环境搭好只是开始。组件测试真正要掌握的是三个动作它们分别是挂载、交互、断言。把这三个动作组合起来基本能覆盖绝大多数组件的测试场景。4.1 挂载mount 与 shallowMount 的取舍Vue Test Utils 提供了两个挂载方法mount完整渲染组件包括它的所有子组件。shallowMount只渲染当前组件子组件会以“桩”的形式被替换掉。我在实际操作中体会到的区别是这样的mount更适合验证组件与子组件的真实联动比如父组件传 props 给子组件、子组件触发事件被父组件捕获这类场景shallowMount则更适合只关注当前组件自身行为的场景——比如测试组件内部的点击逻辑、数据渲染、事件派发。shallowMount的优势是隔离性好、速度快不会因为子组件内部数据变化或报错导致当前组件测试失败。但它的缺点是过度模拟可能掩盖集成问题。所以我的习惯是对单个组件做行为验证用shallowMount对涉及父子组件联合场景的测试用mount。另外推荐在测试里启用自动卸载防止组件实例泄漏导致的内存问题import { enableAutoUnmount } from vue/test-utils import { afterEach } from vitest enableAutoUnmount(afterEach)把这个写在测试文件顶部或全局 setup 文件里每个测试用例执行完后自动调用wrapper.unmount()。4.2 交互用 trigger 模拟用户操作trigger是 VTU 里触发 DOM 事件的方式。基础用法是await wrapper.find(button).trigger(click)这里有两个容易翻车的点。第一trigger返回的是 Promise必须await。因为事件处理函数里可能涉及响应式数据的更新如果不等待后面的断言可能拿到的是更新前的旧值。第二trigger模拟的是 DOM 事件它能触发 Vue 的事件绑定但对于真实浏览器里的某些行为比如键盘的 keydown 事件需要用trigger(keydown, { key: Enter })这样传入事件初始化对象。如果是触发自定义事件emit用wrapper.vm.$emit确实可以但更推荐的方式是断言wrapper.emitted()。这个 API 返回组件实例派发的所有事件记录可以验证事件名、参数、次数const wrapper mount(MyComponent) await wrapper.find(button).trigger(click) const emitRecords wrapper.emitted(submit) expect(emitRecords).toHaveLength(1) expect(emitRecords[0]).toEqual([{ username: admin }])4.3 异步与状态更新nextTick 和 flushPromisesVue 的响应式更新是异步的在测试里经常会遇到“明明改了 state但 DOM 没有立刻变”的情况。官方推荐的解决方案是await nextTick()但更稳妥的处理是使用flushPromises()。两者的区别nextTick只等待当前事件循环中的微任务队列清空适合简单的同步数据更新flushPromises会把当前所有已排队和即将排队的 Promise 都执行完适合处理涉及异步接口、setTimeout或Promise.resolve的情况。比如项目中常见的 loading 状态wrapper.find(button).trigger(click) await flushPromises() expect(wrapper.find(.loading).exists()).toBe(false) expect(wrapper.text()).toContain(提交成功)4.4 模拟依赖vi.mock 与 vi.spyOn组件通常会调用接口请求、路由跳转、状态管理等外部依赖。测试时不应该让这些真实执行而是用模拟替换。Vitest 提供了完整的 mock 能力。拦截模块级依赖用vi.mockimport { vi } from vitest vi.mock(/services/user, () ({ login: vi.fn().mockResolvedValue({ token: fake-token }), }))监听对象的方法用vi.spyOnimport { useRouter } from vue-router vi.mock(vue-router, () ({ useRouter: vi.fn(), })) const push vi.fn() useRouter.mockReturnValue({ push }) // 在测试中触发跳转 expect(push).toHaveBeenCalledWith(/dashboard)这里有一点要提醒mock 的作用域和清理。Vitest 默认每个测试文件是隔离的但 mock 的顺序有讲究——如果你在文件顶部 mock 了一个模块测试文件里 import 这个模块时拿到的直接就是 mock 后的版本。如果你希望不同用例有不同返回值用mockResolvedValueOnce或mockImplementationOnce来控制。4.5 断言的完整视角组件测试的断言可以从几个维度看DOM 文本内容、元素是否存在、props 是否被正确传入、事件是否被正确派发、样式是否应用、class 是否变化。优先断言用户可感知的行为比如“页面上出现了某段文案”“按钮变成禁用状态”而不是断言组件内部的某个中间状态。内部实现细节测得太死重构时明明只是改了内部写法测试却因为实现变化而挂了这就失去了测试保护行为的意义。5. 一个真实场景用例登录表单从单元测试到端到端测试把核心 API 讲完后看一个稍微完整一点的例子这样整条链路会更清晰。我选的是登录表单因为几乎每个后台管理系统都有而且它涉及表单校验、异步请求、状态反馈、路由跳转对测试来说样本够丰富。5.1 登录组件结构template form submit.preventhandleSubmit div label forusername用户名/label input idusername v-modelusername / /div div label forpassword密码/label input idpassword typepassword v-modelpassword / /div p v-iferrorMessage classerror{{ errorMessage }}/p button typesubmit :disabledloading {{ loading ? 登录中... : 登录 }} /button /form /template script setup langts import { ref } from vue import { useRouter } from vue-router import { login } from /services/user const router useRouter() const username ref() const password ref() const errorMessage ref() const loading ref(false) const handleSubmit async () { errorMessage.value if (!username.value || !password.value) { errorMessage.value 用户名和密码不能为空 return } loading.value true try { const res await login({ username: username.value, password: password.value }) localStorage.setItem(token, res.token) router.push(/dashboard) } catch { errorMessage.value 登录失败请检查用户名或密码 } finally { loading.value false } } /script5.2 单元测试用例设计针对这个组件我设计了下面几个用例import { describe, it, expect, vi, beforeEach } from vitest import { mount, flushPromises } from vue/test-utils import LoginForm from ../LoginForm.vue vi.mock(/services/user, () ({ login: vi.fn(), })) vi.mock(vue-router, () ({ useRouter: () ({ push: pushMock }), })) const pushMock vi.fn() describe(LoginForm, () { beforeEach(() { pushMock.mockClear() localStorage.clear() }) it(用户名为空时提交会显示错误提示, async () { const wrapper mount(LoginForm) await wrapper.find(button).trigger(click) expect(wrapper.find(.error).text()).toContain(用户名和密码不能为空) }) it(调用登录接口成功后跳转路由, async () { login.mockResolvedValue({ token: test-token }) const wrapper mount(LoginForm) await wrapper.find(#username).setValue(admin) await wrapper.find(#password).setValue(123456) await wrapper.find(button).trigger(click) await flushPromises() expect(login).toHaveBeenCalledWith({ username: admin, password: 123456 }) expect(localStorage.getItem(token)).toBe(test-token) expect(pushMock).toHaveBeenCalledWith(/dashboard) }) it(登录接口失败时展示错误提示, async () { login.mockRejectedValue(new Error(invalid)) const wrapper mount(LoginForm) await wrapper.find(#username).setValue(admin) await wrapper.find(#password).setValue(wrong) await wrapper.find(button).trigger(click) await flushPromises() expect(wrapper.find(.error).text()).toContain(登录失败) }) })这段测试里关键的细节有setValue是 VTU 设置表单输入值的专用 API它会触发对应事件并更新 v-model 绑定。login因为是 vi.mock 后的模块所以可以直接导入并被mockResolvedValue/mockRejectedValue控制。每次用例执行前mockClear和localStorage.clear()很重要否则用例之间会互相影响。5.3 端到端测试同样的登录流程换成浏览器执行单元测试验证的是 LoginForm 组件本身但它无法回答一个问题真实浏览器里用户用这个页面时输入、点击、跳转整条链路能不能通。这就是 E2E 的用武之地。先安装 Playwrightnpm install -D playwright/test npx playwright installnpx playwright install会下载对应的浏览器内核。如果下载缓慢可以设置环境变量指向国内的镜像源。创建playwright.config.tsimport { defineConfig, devices } from playwright/test export default defineConfig({ testDir: ./e2e, timeout: 30000, use: { baseURL: http://localhost:5173, trace: on-first-retry, }, projects: [ { name: chromium, use: { ...devices[Desktop Chrome] }, }, ], webServer: { command: npm run dev, url: http://localhost:5173, reuseExistingServer: true, }, })这里的webServer配置很关键。它让 Playwright 在跑测试之前自动启动开发服务器测试结束再自动关闭省去手动开服务的麻烦。reuseExistingServer: true表示如果开发服务器已经在跑就复用不重复启动。然后编写 E2E 登录测试import { test, expect } from playwright/test test(用户可以通过登录表单进入后台, async ({ page }) { await page.goto(/login) await page.fill(#username, admin) await page.fill(#password, 123456) await page.click(button[typesubmit]) await expect(page).toHaveURL(/\/dashboard/) await expect(page.locator(text欢迎回来)).toBeVisible() })这几行代码在真实浏览器里完成的事情比单元测试更多一层页面能打开、输入框可操作、表单能提交、路由能跳转、跳转后的页面内容真实渲染出来。这里 Playwright 的自动等待会帮我们处理时序问题表单提交后如果还在 loadingtoHaveURL会等待 URL 变化不需要手动 sleep。如果你觉得手写 E2E 还是麻烦可以先用 Playwright 的 codegen 功能自动生成代码npx playwright codegen http://localhost:5173/login然后用自动生成的代码改成测试用例效率高很多。6. 测什么不测什么我对测试用例设计的一点取舍工具会用之后真正影响测试价值的是用例设计。这个阶段我特别想强调一点不是所有代码都值得写测试也不是测试覆盖率越高越好。测试也有成本维护一套不断变化的测试用例成本十分可观。6.1 测试金字塔的启示测试金字塔是一句老生常谈但在 Vue 项目里依然适用底部大量的单元测试往上少量的集成测试顶部极少的端到端测试。为什么是这个结构因为越往下的测试运行越快、定位问题越精确越往上的测试越慢、越难排查。具体到 Vue 项目里我的优先级排序是工具函数和纯函数测试成本最低价值最稳定比如日期格式化、金额转换、状态机流转逻辑。关键组件的用户行为比如“填写表单并提交”“点击按钮切换弹窗”这类行为一旦出错用户直接能感知到。复杂计算属性和 store 的 getter这些往往是业务规则的核心数据出问题最隐蔽。模板样式细节、第三方组件库的展示行为这一类优先级最低甚至可以完全跳过。6.2 别为了覆盖率而写测试有一个常见的误区是团队规定“单元测试覆盖率必须达到 80%”然后开发者为了凑数字连那种只负责展示的静态组件也要写个渲染测试。这种测试意义不大反而增加维护成本。覆盖率指标只代表“这行代码被执行过”不代表“这行代码被正确验证过”。我更推荐一种思路把测试设计聚焦在“高价值的业务行为”上。每次写完代码问自己一个问题如果这个函数或这个组件出了问题用户会怎样发现如果用户完全感知不到那它的测试优先级就很低。6.3 组件测试的边界路由逻辑交给 E2E组件测试里经常会遇到“点击按钮之后应该跳转到某个路由”的情况。我的处理方式是用 mock 的 router 对象断言push被调用了而不是真的在测试里启动一个路由。真正验证路由跳转、路由守卫、动态路由加载这些行为的应该交给 E2E 去做。因为组件测试的环境是模拟的 DOM它无法体现真实浏览器的路由行为差异。6.4 store 逻辑单独测试剥离 UI 更好测如果项目用了 Piniastore 里的状态逻辑完全可以脱离组件单独写成普通测试。store 是纯 TypeScript 模块不需要挂载组件测试速度反而更快import { setActivePinia, createPinia } from pinia import { useUserStore } from /stores/user describe(user store, () { beforeEach(() { setActivePinia(createPinia()) }) it(登录成功后写入 token, async () { const store useUserStore() await store.login({ username: admin, password: 123456 }) expect(store.token).toBe(test-token) }) })这样做的好处是 store 逻辑的回归测试非常快也不依赖任何 DOM 环境。6.5 接口层测试别拿真实接口做测试前端测试里最容易出错的一个地方是接口层。单元测试和 E2E 测试都应该避免真实请求外部服务。单元测试里用 mock 直接替换请求函数E2E 测试里可以用 Playwright 的page.route拦截网络请求返回预设数据。这样才能保证测试的可重复性和稳定性。await page.route(**/api/login, route { route.fulfill({ status: 200, contentType: application/json, body: JSON.stringify({ token: fake-token }), }) })7. 实操中踩过的坑与收尾建议这一节是当天折腾下来最有价值的部分。很多问题在官方文档里写得并不显眼但一旦踩进去排查起来非常耗时。7.1 jsdom 环境缺少浏览器 APIjsdom 能模拟 DOM但不等于完整浏览器。测试中遇到频率最高的两个报错一个是localStorage is not defined在旧版本 jsdom 里可能出现另一个是matchMedia is not a function很多组件库的栅格、响应式判断会调用window.matchMedia。解决方案是提前在测试 setup 文件里补全这些 API。在 vite.config.ts 里配置 setup 文件test: { environment: jsdom, globals: true, setupFiles: [./src/test/setup.ts], }然后在src/test/setup.ts里补上兼容代码if (!window.matchMedia) { window.matchMedia () ({ matches: false, addListener: () {}, removeListener: () {}, addEventListener: () {}, removeEventListener: () {}, }) } if (!window.ResizeObserver) { window.ResizeObserver class { observe() {} unobserve() {} disconnect() {} } }7.2 异步组件和 Suspense 的测试如果组件内部用了动态defineAsyncComponent或者Suspense测试时组件渲染通常是异步的直接断言文本内容会拿到空值。处理方式依然是等待异步操作完成await flushPromises()如果 flushPromises 仍然不够可以加上一个真实时间戳的等待但这是最后手段不建议滥用。7.3 快照测试别用于大型组件快照测试toMatchSnapshot看起来很省事但大型组件的快照一旦变化diff 非常难读。尤其是组件里带了动态生成的 id、时间戳、随机数据时快照极不稳定几乎每次跑测试都会失败。我的建议是快照只用于极小的、稳定的展示型组件并且尽量在生成之前把动态内容 mock 掉。7.4 路径别名和 TS 配置不一致Vitest 会继承 vite.config.ts 的 resolve.alias但如果你的项目里用的是 tsconfig paths 而 vite.config 里没有配置测试在解析/xxx这种导入时依然可能失败。这里我踩过的一个坑是项目里 vite.config 的 alias 配的是tsconfig 配的是/*看起来一致但 Vitest 在某些版本下对 alias 的匹配过于严格最后是把 alias 统一成指向 src 后才解决问题。7.5 E2E 选择器与真实用户行为的一致性Playwright 推荐使用getByRole、getByText、getByLabel这类“面向用户”的定位器而不是直接依赖 CSS 类名。因为真实用户不会关心 class 叫什么关心的是页面上的“登录按钮”“用户名输入框”这些语义元素。使用语义化定位器的好处是重命名了 CSS 类名但测试不用改相反如果大量依赖 CSS 选择器样式调整一次测试就得跟着改一次。7.6 给走到这一天的你的建议如果你正在跟着学习路线推进到了测试这一关我的建议是先别急着把项目里的所有组件都补齐测试。我自己的节奏是先找一个业务频度高、逻辑相对复杂、出过错或即将重构的组件给它写一套完整的单元测试从环境配置到断言全部跑通。之后再把这个流程复制到其他组件上。等单元测试稳定了再花一两个小时给项目里最核心的用户路径配上一两个 E2E 用例。这个节奏能让你在成本可控的前提下最快地感受到测试带来的安全感。我在第 57 天结束的时候终于敢在一个旧项目里做之前一直不敢碰的重构了。原因并不是那天之后代码就突然不会出 bug而是我知道回归的时候测试会替我先检查一遍。这种心理上的踏实就是测试这个阶段真正的收获。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价