ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

单元测试落地指南:设计、Vue组件测试与LLM辅助的实战经验

单元测试落地指南:设计、Vue组件测试与LLM辅助的实战经验 带过几个项目组之后发现一个挺反直觉的现象大家都在嘴上承认单元测试重要可真到排期的时候它往往是第一个被砍掉的需求。不是团队懒而是很多人心里没底——不知道测什么、不知道怎么测才能不拖累进度、更不知道写出来的测试到底守住了多少风险。这篇文章想从一个我实际跟完的项目说起把单元测试从理论正确到落地可用这个过程中的判断、踩坑和收益都摊开讲。不管你是刚接触测试的初级开发还是想推动团队改进测试现状的技术负责人应该都能在里面找到一些能直接拿去用的东西。另外我会专门用一节讲 Vue 组件测试里那些翻来覆去出现的高频报错以及这几年大家都在聊的 LLM 辅助生成测试到底是真的提效还是又一个新包袱。1. 为什么单元测试道理都懂落地就废我见过太多团队在单元测试上栽跟头症状几乎一模一样一开始热情高涨定覆盖率指标、搭测试框架两周之后测试用例开始变红三个月后测试代码成了谁都不想碰的遗产。问题的根源往往不在于测试本身而在于用错了思路去对待它。1.1 测试不是写出来的是设计出来的很多人拿到一个需求第一反应是等我写完功能再来补测试。这个顺序本身就注定了测试会成为负担。功能代码没有为可测试性做过任何设计到处都是直接 new 出来的依赖、藏在函数内部的副作用、动不动就访问 window 或 localStorage 的隐性耦合。等你回头补测试的时候要么得动用各种 mock 技巧去绕过这些障碍要么干脆发现这段代码根本没法在测试环境里跑起来。我在项目里反复强调一件事测试要参与设计而不是事后补账。一个函数如果难以测试往往不是测试方法的问题而是这个函数本身承担了太多职责。比如下面这段代码是我经常拿来当反例的// 反例职责混杂难以测试 export function handleSubmit(formData) { const validated validateForm(formData); if (!validated.isValid) { showToast(validated.errors.join(,)); return; } const payload transformPayload(formData); const result apiClient.submit(payload); if (result.ok) { trackEvent(submit_success); router.push(/success); } else { showToast(提交失败请重试); } }这个函数涉及表单校验、UI反馈、数据转换、接口调用、埋点、路由跳转至少六个职责。要测它你得 mock 掉 apiClient、router、埋点工具、toast 组件。测试本身写起来痛苦跑起来还担惊受怕——因为你根本不知道断言什么才算真正验证了业务逻辑。重构后的版本会把纯逻辑拆成独立的、依赖注入的函数// 正例单一职责可注入依赖 export function buildSubmitFlow({ apiClient, router, toast, trackEvent }) { return { async execute(formData) { const validated validateForm(formData); if (!validated.isValid) { toast.warn(validated.errors.join(,)); return { status: validation_failed, errors: validated.errors }; } const payload transformPayload(formData); const result await apiClient.submit(payload); if (!result.ok) return { status: submit_failed }; trackEvent(submit_success, { orderId: result.orderId }); router.push(/success); return { status: success, data: result }; } }; }你看核心逻辑拆出来之后测试只需要注入假的 apiClient 和 router就能把各种分支全部覆盖到。实际项目中很多团队发现测试难写的真正原因是代码难测而代码难测的背后是设计上没有为变化和验证留出空间。1.2 先解决测什么的问题再谈怎么测新人最爱问的一句话是这个功能怎么测。但其实更该问的是这个功能的关键行为是什么。单元测试不是把函数的每一行代码都跑到而是把这个单元对外承诺的行为验证一遍。我在需求评审阶段就会做一件事把每个模块的核心行为列成一个清单。比如对于一个订单列表页面核心行为可能是加载中显示骨架屏加载完成后渲染列表列表为空时展示空状态和引导文案点击取消订单按钮后弹出确认框确认后调用取消接口取消成功后列表刷新并且 toast 提示成功取消失败时保留原列表且展示错误信息你看这不是照着函数抄出来的测试点而是从用户可感知的行为倒推出来的。带着这个清单去写测试你就不会纠结这个中间变量要不要断言之类的问题因为你的关注点始终在行为契约上。一个很实用的技巧测试用例的组织方式不要按函数名拆而是按行为场景拆。比如订单列表.spec.js里面不是 test 一个fetchList、test 一个cancelOrder而是一个describe(取消订单流程)下面挂五六个 it覆盖上面列出的各种场景和分支。这样测试本身就是一份活文档新同学看一遍测试就能知道这个模块最重要的行为是什么。2. 一个真实项目的测试方案设计光说方法论太虚我拿自己最近跟的一个项目来复盘。这是一个管理后台的订单模块技术栈是 Vue 3 TypeScript Pinia测试工具选的是 Vitest Vue Test Utils。整个模块有四十多个文件我在做技术方案的时候给它定了一个三层测试策略。2.1 测试金字塔落地三层各自守什么这套策略不是拍脑袋定的而是基于成本和收益的权衡。我把测试分成三层层级覆盖对象工具目标第一层纯函数与工具格式化、校验、状态映射、数据转换Vitest 直接跑逻辑正确性覆盖率尽量高第二层组合式函数与 Store业务逻辑、状态流转、异步编排Vitest 手写依赖注入行为正确性关注边界条件第三层组件交互组件渲染、事件交互、用户操作反馈Vitest Vue Test Utils组件行为契约不过度追求覆盖率这个分层背后的逻辑很朴素纯函数最好测收益也最直接所以用力最猛。业务逻辑是容易出 Bug 的地方而且修起来代价高所以要重点守。组件交互最容易被各种环境问题干扰而且 UI 细节的断言维护成本高所以只测关键交互路径。实际执行下来我们的测试代码和业务代码行数比大概在 1:1.5 左右听起来写了挺多测试代码但因为层次分得清楚维护起来没有想象中那么痛苦。纯函数那边基本不怎么动组件测试只有在交互逻辑有调整时才需要跟着改。2.2 纯函数层把边界条件焊死在测试里我先说纯函数层因为这是整个测试方案里性价比最高的部分。比如订单模块里有一个formatOrderStatus(status, locale)的函数把后端返回的状态码映射成前端展示文本。这个函数当初被列为核心行为清单的第一条因为它直接决定了页面上用户看到什么。这种函数说白了就是个查表逻辑代码实现毫无难度但边界情况特别多。后端的状态码可能随时新增不同的 locale 文案不一样还有几种特殊的前端合成状态需要额外处理。我用测试把它焊死import { describe, it, expect } from vitest; import { formatOrderStatus } from /utils/orderStatus; describe(formatOrderStatus, () { it(应该正确映射基础订单状态, () { expect(formatOrderStatus(PENDING, zh-CN)).toBe(待支付); expect(formatOrderStatus(PAID, zh-CN)).toBe(已支付); expect(formatOrderStatus(SHIPPED, zh-CN)).toBe(已发货); expect(formatOrderStatus(COMPLETED, zh-CN)).toBe(已完成); expect(formatOrderStatus(CANCELLED, zh-CN)).toBe(已取消); }); it(应该正确映射英文文案, () { expect(formatOrderStatus(PENDING, en-US)).toBe(Pending); }); it(未知状态码应该返回兜底文案而不是抛错, () { expect(formatOrderStatus(UNKNOWN_STATUS, zh-CN)).toBe(未知状态); }); it(空状态码应该返回兜底文案, () { expect(formatOrderStatus(, zh-CN)).toBe(未知状态); }); it(不应该出现遗漏的状态分支, () { const allStatuses [PENDING, PAID, SHIPPED, COMPLETED, CANCELLED]; const handledStatuses allStatuses.filter((status) { return formatOrderStatus(status, zh-CN) ! 未知状态; }); expect(handledStatuses).toEqual(allStatuses); }); });最后一个测试用例是挺有意思的一个小技巧——用应该出现什么来反向校验。如果后端哪天真加了新状态码而前端的映射表忘了同步这个用例会直接报错提醒你去更新映射。这比靠 code review 去人工核对可靠得多。你可能注意到我没有对每个状态都搞一个独立的it(当传入 X 时应该返回 Y)。因为那些断言的本质是一样的只是输入输出不同。这里用参数化测试会更优雅Vitest 支持it.eachit.each([ [PENDING, zh-CN, 待支付], [PAID, zh-CN, 已支付], [SHIPPED, en-US, Shipped], ])(格式化状态 %s(%s) 返回 %s, (status, locale, expected) { expect(formatOrderStatus(status, locale)).toBe(expected); });这样既保证了覆盖又避免了测试代码自身的重复维护。我后来在项目里定了一个不成文的规定同一逻辑、不同输入的断言一律参数化不写成重复的 it 块。2.3 业务逻辑层组合式函数和 Store 怎么测第二层测的是 Vue 3 的组合式函数和 Pinia 的 store。这层是业务逻辑最容易腐烂的地方因为涉及异步请求、状态流转、错误处理还往往有多个函数之间的状态依赖。举一个实际的例子。订单列表有一个useOrderList的组合式函数封装了加载列表、分页、筛选、重新加载的逻辑。它内部调用了一个usePagination的通用组合式函数来管理分页状态。结构大概是这样的export function useOrderList(options {}) { const { apiClient } options; // 支持注入测试时传入 mock const pagination usePagination(); const orders ref([]); const loading ref(false); const error refstring | null(null); async function loadOrders() { loading.value true; error.value null; try { const res await apiClient.fetchOrders({ page: pagination.page.value, pageSize: pagination.pageSize.value, status: pagination.filters.value.status }); orders.value res.list; pagination.setTotal(res.total); } catch (e) { error.value 订单加载失败请稍后重试; } finally { loading.value false; } } function changePage(page) { pagination.setPage(page); } watch(() pagination.page.value, loadOrders); return { orders, loading, error, loadOrders, changePage, ...pagination }; }测试这段逻辑的时候核心关注点有这么几个首次加载成功后数据是否正确写入orders、加载失败时error是否被正确设置、loading状态是否在请求前和请求后都符合预期、翻页后是否自动触发重新加载。下面是用 mock 的 apiClient 来测的import { describe, it, expect, vi, beforeEach } from vitest; import { useOrderList } from /composables/useOrderList; function createMockApiClient() { return { fetchOrders: vi.fn() }; } describe(useOrderList, () { let mockApi; beforeEach(() { mockApi createMockApiClient(); }); it(成功加载订单列表后更新数据和总数, async () { mockApi.fetchOrders.mockResolvedValue({ list: [{ id: 1, amount: 100 }], total: 1 }); const { orders, loading, pagination } useOrderList({ apiClient: mockApi }); await loadOrders(); expect(orders.value).toEqual([{ id: 1, amount: 100 }]); expect(pagination.total.value).toBe(1); expect(loading.value).toBe(false); }); it(加载失败时设置错误信息且不清空已有数据, async () { mockApi.fetchOrders.mockRejectedValue(new Error(network error)); const { orders, error, loadOrders } useOrderList({ apiClient: mockApi }); await loadOrders(); expect(error.value).toBe(订单加载失败请稍后重试); expect(orders.value).toEqual([]); // 初始数据为空失败后仍为空 }); });这里有个容易忽略的点useOrderList内部通过watch(() pagination.page.value, loadOrders);监听了页码变化。在测试里调用changePage(2)的时候因为watch默认不是立即执行你需要等一个 tick。这时可以借助flushPromises或者nextTick来保证异步更新完成。实战中这类测试里忘了等 nextTick导致的假失败特别常见。对这种异步编排逻辑我还有一个建议把关键异步流程抽成独立的方法并显式调用不要让所有逻辑都依赖 watch 触发。watch 是隐式的测试时如果忘记触发条件会误以为功能有问题而显式调用则清晰可控。我们的loadOrders本身就支持直接调用watch 只是作为翻页时的自动触发补充这样两边都兼顾了。3. Vue 组件测试的高频报错与排查实录讲完了纯函数和业务逻辑层来到第三层组件测试。说句实话这一层是很多团队在单元测试上折戟沉沙的地方。Vue 组件本身的异步渲染机制、各种指令和生命周期钩子、以及与 DOM 环境的交互让组件测试比纯逻辑测试复杂一个量级。这一节我整理几个之前团队里反复踩到的高频报错和排查思路每一个都是真实遇过的。3.1 报错一Cannot find module /components/X.vue的路径解析问题这个问题几乎每个从 Jest 切到 Vitest 或者新起 Vitest 项目的团队都会遇到。报错发生在测试文件里import组件的时候明明业务代码跑得好好的一跑到测试里就找不到模块。原因通常很朴素Vitest 默认不会读取 Vite 的resolve.alias之外的自定义路径配置或者 alias 的配置在 vite.config 中写的不够完整。如果你用的是/这种 alias需要确保在 Vitest 的配置里同步 alias。一个典型的修复方式是这样// vitest.config.ts import { defineConfig } from vitest/config; import path from path; export default defineConfig({ resolve: { alias: { : path.resolve(__dirname, src) } }, test: { environment: jsdom } });如果 alias 配好了还是报错再看一下是不是tsconfig.json里面 paths 的 baseUrl 设置有问题因为 Vitest 的类型检查依赖 tsconfig。有时候两个配置一个用的是src/*另一个用的是./src/*这种细小的不一致就会导致找不到。我的经验是新建 Vitest 项目时第一时间先写一个最简单的空组件测试跑通路径解析再开始写业务测试。如果连空组件都 import 不了后面所有的排查都会混在一起很难定位。3.2 报错二wrapper.find()找不到元素但明明渲染出来了这个报错的经典场景是这样的测试里wrapper.find([data-testidorder-row])返回了一个空的 wrapper断言exists()为 true 失败。但你打开浏览器或者用wrapper.html()打印出来发现元素明明就在那里。这里九成是渲染时机的问题。Vue 组件的更新是异步的你的组件里可能在onMounted里触发了一个异步请求请求回来后数据才渲染出来。测试里同步执行完mount之后数据还没回来这个时候去 find 当然是空的。典型场景代码如下const wrapper mount(OrderList, { props: { orders: [] } }); // 这里如果组件内部在 onMounted 里异步拉数据下面这一行大概率拿不到内容 expect(wrapper.find([data-testidorder-row]).exists()).toBe(true);解决办法是让测试环境等异步任务完成。常用的做法是用flushPromisesimport { flushPromises } from vue/test-utils; const wrapper mount(OrderList); await flushPromises(); expect(wrapper.find([data-testidorder-row]).exists()).toBe(true);flushPromises会排空微任务队列里的所有 Promise保证那些async方法执行到了返回结果之后。这算是一个保平安的工具但注意它只对 Promise 有效如果涉及 setTimeout 之类的宏任务你还是需要用vi.useFakeTimers()或者手动等待。实战建议如果组件里有定时器、动画、延迟加载这类基于宏任务的逻辑尽量在测试里用vi.useFakeTimers()配合vi.advanceTimersByTime()来控制时间否则测试会变得又慢又不稳定。3.3 报错三[Vue warn]: Missing required prop导致的测试崩溃刚写组件测试的同事经常会遇到这个问题。组件声明了一个必填的 props测试挂载时没传或者传错了类型Vue 直接警告要命的是在很多配置下这个警告会变成测试失败的一部分。这种问题通常分两种一种是真的漏传了必填 props这种直接补上就行。另一种是组件设计上把不该必填的属性设成了必填。比如一个按钮组件type属性有默认值却在 props 声明里写了required: true这就会导致每次测试都要显式传一次。遇到这种报错我建议先想一下组件设计是不是合理。props 的必填属性应该是没有它组件就无法工作的那些而不是有默认值但外部偶尔会覆盖的那些。把默认值放到withDefaults里测试代码也能少写很多无意义的传参。顺带提一个 Vue 组件测试的配置项global.config.warnHandler或config.compilerOptions。如果你明确知道组件内部会产生某些非关键警告可以在测试配置里忽略但我不推荐一上来就这么干。忽略警告会让你漏掉真正的配置错误我通常是在确认警告无害之后才在最小范围内把它按掉。3.4 报错四Cannot read properties of undefined在组件里调用某个方法时这个报错经常出现在组件测试触发了某个事件处理函数但这个函数内部访问了this.$router、this.$store或某个全局插件提供的属性。挂载的时候没有安装对应插件运行时就爆 undefnied。Vue Test Utils 里可以通过global.mocks来快速提供这些全局属性const wrapper mount(OrderCancelButton, { global: { mocks: { $router: { push: vi.fn() }, $route: { query: {} } } } });注意在 Vue 3 Pinia 的环境里store 通常不通过$store访问而是通过useStore()在组件内部访问。这种情况下需要在挂载之前确保 Pinia 已经被激活import { createPinia, setActivePinia } from pinia; beforeEach(() { setActivePinia(createPinia()); });这个setActivePinia特别容易漏。如果你在测试里发现useStore()返回的 store 里状态全是初始值改了也不生效多半就是忘了在测试环境里先把 Pinia 激活。3.5 组件测试的过测不过用问题最后说一个不那么像报错的坑但杀伤力很大。有时候组件测试全绿功能却是坏的。最常见的原因是你写的断言太偏向内部实现而不是用户可感知的行为。比如断言了一个内部变量的值或者直接检查了某个 DOM 的 class name而不是检查用户能看到的关键内容。举一个例子一个登录表单测试断言的是提交按钮的 loading 状态为 true而不是提交后出现加载动画并且输入框被禁用。前者是内部实现细节因为 loading 状态可能随时被重构掉后者才是用户真实感知到的行为。测试一旦陷入测实现重构的时候就会疯狂误报团队久了就再也不信这套测试了。我自己的做法是写组件测试时反复问自己一句——如果组件从 React 实现换成 Vue 实现这套测试能不能照样通过如果不能说明断言盯错了地方。需要修正的目标是测用户可感知的行为不是测现在的代码长什么样。4. 基于 LLM 的单元测试真提效还是新包袱最近圈子里的热门话题是拿 LLM 来辅助写单元测试。作为已经在项目里实际用了一段时间的人我聊聊真实的体感。先说结论LLM 在特定场景下确实能大幅提效但如果你指望它直接给你一份能跑的完整测试那大概率会碰壁。4.1 LLM 辅助测试的效率边界与应用场景我实践下来LLM 在三个场景下是最划算的为纯函数生成边界测试用例。你给它一个函数的定义和几个示例输入输出它能在几秒内给你列出一大串边界条件清单——空字符串、负数、极大值、特殊字符、null、undefined 之类的。这些正是人类最容易漏掉的场景。为已有功能做差异测试生成即生成能够模仿现有功能行为的测试桩方便你在重构时对比新旧行为是否一致。帮你快速把团队已有的测试风格迁移到新项目比如从 Jest 的 API 风格改写为 Vitest或者从 CommonJS 模块改写为 ES ModulesLLM 在模式化改写上确实稳定且高效。拿我最近一个案例来说团队准备把一批老的 Jest 测试迁移到 Vitest。一百多个测试文件人工改至少得一天而且就是纯粹的机械性工作。我试着让 LLM 批量处理给它一份Jest 与 Vitest 常见 API 对照表和三个示例文件跑出来的准确率相当高。剩下的少量报错基本都是因为老测试里用了一些 Jest 私有 API手动修一下也就完事了。4.2 生成测试的提示词设计与人工校验闭环先泼一盆冷水LLM 生成的测试代码直接就跑通的概率不高更常见的是看起来像那么回事但一跑就报错。原因在于 LLM 不了解你的项目上下文——不知道别名怎么配、不知道 mock 数据长什么样、不知道组件有几个 props。所以不能让它凭空生成然后去跑得给它喂足上下文并且设计好提示词。我常用的提示词模板大概是这样的我在一个 Vue 3 TypeScript Vitest 的项目里工作需要为下面的函数写单元测试。 函数位于 src/utils/orderStatus.ts完整代码如下 [函数代码] 项目有以下测试约定 1. 使用 describe/it/expect 风格 2. 导入路径统一使用 / 别名 3. 测试文件放在与源文件同级的 __tests__ 目录 4. 需要覆盖正常情况、边界情况和异常情况不要遗漏空值场景 请生成可直接运行的测试代码。如果某段代码无法测试请明确说明原因不要编造 mock。关键就一句不要编造 mock。因为 LLM 最大的问题是会一本正经地替你脑补不存在的依赖如果它觉得函数里要调 API就会自作主张给你写一个假的 axios 实例。加了这句约束之后生成的代码质量有明显提升。但这还只是个开始。我把 LLM 辅助写测试的流程固定为四步生成LLM 基于函数代码和约定输出测试文件。运行直接把测试代码丢进 Vitest 跑记录所有报错。回传把报错信息原样发给 LLM让它自省和修正。人工复核由有经验的开发者检查测试断言的正确性确认它在测行为而不是测实现。其中第四步无论如何都不能省。LLM 生成的测试经常会把错误的实现行为也当成正确行为来断言这叫过拟合测试。比如一个明明是返回错误的函数LLM 会照着错误行为生成测试然后测试通过了。这种时候如果没有有经验的人把关反而可能带来虚假的安全感。4.3 实测案例一万多行代码补测试的坑与收益我在一个用 Vue 2 写的存量模块上做过一次实验这个模块大概一万多行核心逻辑长期没有测试。任务是在不影响功能的前提下把核心纯函数和 store 的测试补齐。过程是先人工抽出所有纯函数清单把每个函数的代码和接口定义发给 LLM 生成初版测试然后批量跑 Vitest把失败信息汇总后再丢回给 LLM 修订。前后处理了两百多个函数人力成本大概四个工作日如果纯人工写按平时的速度得三周左右。但这也有代价。最大的坑是LLM 生成的测试容易在不该 mock 的地方上 mock尤其是对内部工具的 mock。它会很殷勤地把一个本来可以真实执行的工具函数给 mock 掉这样测试就退化成对 mock 数据的断言实际情况完全没测到。我对这类问题的识别办法是看测试里 mock 的依赖清单如果一个测试 mock 了超过三个自身模块的依赖我就要怀疑它在测什么了。经过这轮实验团队最后的共识是LLM 的价值在于缩短从 0 到 1的过程而不在从 1 到 100的质量。初版测试生成交给 LLM质量把控和边界审查必须靠人。5. 常见问题速查与长期维护经验聊完了具体案例和技术热点最后整理一下长期维护单元测试时最常遇到的坑以及我总结出的一些实操习惯。5.1 高频问题速查表问题常见原因排查思路与解法测试跑得越来越慢每个测试文件都启动了完整的 Vue 应用或真实 API 请求检查是否有测试文件真的在发网络请求用 fake timers 替代真实等待考虑用vi.mock把重依赖替换掉测试之间互相影响共享了可变状态但没清理每个beforeEach里重置 Pinia 和全局状态不要在测试里定义只读的顶层变量并在多个用例间复用改了业务代码导致大量测试挂掉断言写得太贴近内部实现回到行为契约视角优先修改断言的粒度而不是简单更新预期值识别哪些测试是在测行为而不是实现覆盖率很高但 Bug 还是漏覆盖率数字是被假测试撑起来的检查是否有大量断言为空接口无关的测试比如只检查函数被调用引入 mutation testing 工具会更真实地暴露测试盲区组件测试频繁出现超时组件的异步渲染没有正确等待多用flushPromises、nextTick如果有定时器用vi.useFakeTimers()控制时间这张表里的问题除了第一个都是我在多个项目里反复遇到的。值得单说的是覆盖率这件事。团队在最早做单元测试时最喜欢定覆盖率必须达到 80%这种一刀切的指标结果就是大家疯狂给工具函数和简单组件补测试真正复杂的业务编排反而没人碰。后来我们干脆放弃了全量覆盖率指标改为只对核心模块设置覆盖率门槛并且要求新增核心逻辑必须有对应的测试用例。效果反而好了很多。5.2 让测试团队愿意维护的几个习惯单元测试最难的其实不是写出来而是三年后还有人愿意维护。根据我的一线体会有几个习惯能明显降低维护成本测试文件和源码放同级目录。不要倒腾一个专门的__tests__目录套在另一个层级里否则动源码的时候经常忘了同步动测试。测试命名要能读出业务含义。it(取消订单成功后刷新列表并提示成功)这种命名三个月后你回来看还能一秒想起来它在守什么行为。it(should work correctly)这种命名基本等于没写。把测试失败当成一个需要追查的事件。团队里有的人遇到失败的测试第一反应是我改的代码是不是破坏了什么这很好但还有个别人第一反应是这个测试是不是过期了然后直接改断言让测试变绿。后者是个非常危险的信号。我一般会要求要修改测试断言必须写一句注释说明为什么旧断言不再合理。如果说不出来那就是在自欺欺人。定期做一次测试代码审查。每季度挑几个核心测试文件像 review 业务代码一样 review 测试代码看断言质量、检查是否还有测试背后在做网络请求、确认没有用不合理的 mock 掩盖真实问题。这个过程能及时避免测试资产的腐化。最后分享一个小小的实操技巧。我把测试里的mount统一封装了一层createWrapper函数里面做了默认的全局插件安装、store 初始化、自定义指令注册。这样业务测试代码就不用每次写一大堆 boilerplate新同学上手也特别快。这个封装看起来不起眼但它真真切切把团队写组件测试的意愿拉高了一截因为开始写一个测试这件事的门槛变低了。单元测试这件事说到底比的不是谁写得快而是谁家的测试能在项目演进过程中持续提供安全感。我从实践的体会是别急着追求覆盖率数字先让一批核心行为的测试真正跑起来、让人尝到改代码不怕改错的甜头。一旦这种甜头在团队里传播开单元测试就不再是流程要求的任务而是大家自发想做的事。这个转变比任何技术方案都重要。
返回列表