资讯详情

资讯详情

单元测试值不值得写?从原理到LLM辅助生成一次讲透

先聊一个很现实的问题我见过太多项目业务跑得风生水起一看代码仓库测试文件数量是零。问起来原因答案高度一致——“写测试太花时间了”“功能都做不完还写什么测试”“测试代码又不会上线写了有什么用”。但同样也是这些人在某个深夜因为一个不起眼的改动把线上搞挂了然后花整整两天定位问题。单元测试这东西平时看着像在做慈善出问题的时候才知道是救命稻草。这篇就把单元测试讲透它到底是什么在项目里到底扮演什么角色怎么写出有价值的用例以及现在基于LLM的测试生成到底能不能帮上忙。我尽量把话说明白不堆概念。看完这一篇你应该能做到三件事第一清楚知道单元测试到底在测什么、帮我们挡什么风险第二能判断自己项目里哪些代码真正值得写测试第三不管是自己动手写还是借助AI工具都能写出一手不像“为了测试而测试”的用例。1. 单元测试的本质你是在给代码立规矩1.1 一段最短的单元测试长什么样单元测试听起来玄乎拆开就是“单元”加“测试”。单元指的是被测对象里的最小独立单元习惯上指一个函数或一个方法测试就是给定输入、断言输出。拿一段简单的计算逻辑来举例比如判断一个年份是不是闰年function isLeapYear(year) { if (year % 400 0) return true; if (year % 100 0) return false; return year % 4 0; }对应的单元测试长这样test(2000年是闰年, () { expect(isLeapYear(2000)).toBe(true); }); test(1900年不是闰年, () { expect(isLeapYear(1900)).toBe(false); }); test(2024年是闰年, () { expect(isLeapYear(2024)).toBe(true); }); test(2023年不是闰年, () { expect(isLeapYear(2023)).toBe(false); });这个例子简单到有点无聊但恰恰把单元测试的全部核心说完了调用一个独立函数传入特定的输入验证返回值是否符合预期。真正的业务代码比这个复杂得多但底层的思考模式一摸一样。1.2 单元测试不是在测“功能”是在测“规则”很多人有个误解觉得单元测试是把整个功能跑一遍看能不能用。那是集成测试或者端到端测试的活儿。单元测试的格局要小得多它专注的是代码里的某一条规则。举个例子。你在做一个电商系统结算时需要计算用户实付金额规则是满100减20会员再打95折两种优惠可以叠加。这个规则如果没人把它钉死过两个月大概率就会被改出问题——比如某个人把“满100减20”直接改成了“满100打8折”然后其它调用方还以为是原来的规则。单元测试就是为了钉死这种规则而存在的。function calcPayAmount(originalAmount, isMember) { let amount originalAmount; if (amount 100) amount - 20; if (isMember) amount * 0.95; return Math.round(amount * 100) / 100; }test(非会员满100减20, () { expect(calcPayAmount(120, false)).toBe(100); }); test(会员满100后先减20再打95折, () { expect(calcPayAmount(120, true)).toBe(95); }); test(非会员不足100元不减免, () { expect(calcPayAmount(80, false)).toBe(80); });写这三个用例时你实际上把那句“满100减20、会员95折、可叠加”翻译成了机器可以反复验证的断言。以后任何人改这段逻辑只需跑一遍测试规则有没有被破坏一目了然。这就是单元测试最大的价值所在它不是为了防止你出错是为了防止别人包括三个月后的你自己改错。1.3 为什么单元测试要快、要独立衡量单元测试写得好不好有两个硬指标快独立。快是指毫秒级跑完。因为单元测试需要频繁执行每改几行代码你可能就要跑一次如果跑一次要等一分钟你会下意识地减少跑测试的频率那测试的意义就大打折扣。独立是指每个测试用例互不依赖任何一个用例单独跑都应该能通过不能因为前面测试执行过、改了什么状态后面的测试结果就受影响。单元测试、集成测试、端到端测试三者之间的关系用生活中的场景打个比方单元测试就像早上出门前检查钥匙、手机、钱包各就各位集成测试相当于上车后检查车门关上、安全带扣好端到端测试则是全程跑一趟确认从出发到到达一切正常。三种检查各有各的意义谁也替代不了谁。2. 单元测试到底值不值得写这才是真正的关键问题2.1 测试的ROI投入产出比怎么算问“单元测试有什么用”的人潜台词往往是“写它对我到底有什么好处值不值得我的时间”。这个问题很现实也确实要摸着良心算账。写一个单元测试成本大约几分钟到几十分钟不等收益在什么时候兑现在你以后每一次修改这段代码的时候。假设一个工具函数被十个地方调用改动时你肉眼检查可能要看十处调用点还未必看得全如果它有测试直接全部跑一遍几分钟内就能确认影响面。这就是杠杆效应。我见过一个支付模块里面一个计算分账比例的函数前前后后被四个项目复用。有一次产品提出要改分账规则主程原本面如死灰准备花一下午排查所有调用点结果因为有完整的单元测试兜底改完函数体、跑一遍测试十分钟确认全部场景正常。那十几个测试用例的“回本”速度超出预期。2.2 哪些代码值得写测试哪些代码不需要也不是所有代码都要写单元测试。我提供一个简单粗暴的筛选标准凡是包含核心业务规则的代码必须要有测试凡是纯粹搬运数据、没有逻辑分支的代码可以不伺候。值得写测试的优先级排序工具函数、计算逻辑尤其涉及金额、日期、状态转换的后端接口里的业务处理层有判断、有条件分支的地方状态管理中的复杂reducer或store复用率高的组件或函数库负责解析、格式化数据的逻辑优先级比较低、不太值得花时间的简单的getter/setter只有一行return的薄封装纯UI展示组件前提是没有交互逻辑一次性脚本关键不是“写得多”而是“写在刀刃上”。有五个高质量用例的模块跑起来效率远高于五十个低价值用例的模块。2.3 单元测试对重构意味着什么敢不敢动代码的底气重构这件事最大的心理障碍不是能力不够是怕改坏东西。没有测试的代码就像在雷区里闭眼走路每一步都心惊胆战有测试的代码相当于手里拿了个探雷器踩上去之前先扫一遍。说个自己经历的场景。早期写过一个数据处理模块几百行代码逻辑纠缠在一起没人敢动因为它被好几个核心报表依赖。后来我们几个人排了个班先给关键路径补测试把输入输出全部用断言锁死。补到差不多的时候耗时一天。但是从那儿以后这个模块的优化就顺畅多了——我们敢于提取公共函数、改循环逻辑、把嵌套条件压平因为每改一步测试都会告诉我们哪里开裂了。这就是单元测试带来的“重构勇气”。它不是束缚反而是油门让你能踩得更深。3. 实操细节怎么写出真正有价值的单元测试3.1 测什么不测什么先回答一个高频疑问要不要测私有函数答案是视情况而定。如果私有函数的逻辑足够复杂到会出错那就值得测——很多测试框架提供了私有方法测试的手段或者你可以通过导出内部函数的方式测。如果只是一些无逻辑的二传手就没必要刻意去测。再一个高频疑问需不需要把外部依赖数据库、网络请求都mock掉单元测试的基本要求是隔离你测的是A函数就不应该被B服务的响应拖累。常见的做法是把外部依赖注入或者mock掉。具体到实践场景比如测一个从外部拉数据再处理的函数async function fetchAndProcess(url) { const raw await fetch(url); const data await raw.json(); return data.items.map(item ({ id: item.id, price: item.price * 0.9, })); }测试时就不该真的发起网络请求global.fetch jest.fn(() Promise.resolve({ json: () Promise.resolve( { items: [ { id: 1, price: 100 } ] } ), }) ); test(拉取数据后处理成目标结构, async () { const result await fetchAndProcess(https://example.com/api); expect(result).toEqual([{ id: 1, price: 90 }]); });只要涉及外部IO单元测试里默认就要“假装”——假装数据库能用、假装网络通畅、假装第三方接口返回的数据符合预期。这样测试才是可控的跑一百遍结果都一样而不是今天能过明天就不能过。3.2 命名和断言测试代码也是给人读的不少人的测试代码写得很“任性”一个用例就叫test1断言也是堆在一起。真要出现失败光看报告完全看不出是哪条规则被破坏了。我习惯给用例起一个“行为描述式”的名字直接说清预期行为。比如test(会员购买金额满100时先减20再打95折)这样的测试跑挂以后控制台报出来的信息就是“会员购买金额满100时先减20再打95折 —— expected 95 to be 90”哪里出了问题一眼就能找到。断言方面有一个容易忽略的细节优先使用更精确的匹配器而不是笼统的布尔判断。比如查数组长度用toHaveLength查对象包含值用toMatchObject查抛错用toThrow。匹配器越贴切测试失败时给出的提示信息越有用。3.3 边界值、异常路径才是单元测试的试金石别看正常路径测得很欢真正体现测试价值的是边界值和异常分支。还是拿上面的calcPayAmount举例如果只测了120元和80元你漏掉了恰好100元这个边界——这时候满100减20的条件是true还是false代码里判断的是amount 100100应该算满足。但如果代码写成amount 100表面上看正常数据都测不出来恰好100的用户就吃亏了。常见的边界类型包括数量上的最大值、最小值、恰好临界值字符串空串、全空格、超长串数值里的0、负数、小数精度问题数组空数组、只有一个元素、null日期里的2月29日、12月31日、1月1日异常路径也要覆盖。函数里主动抛出的错误、依赖方返回异常时的处理、超时场景等这些都值得写几个用例。3.4 Vue项目配置Vitest从零跑到全绿热搜词里出现了“vue单元测试报错”这不稀奇Vue的测试生态因为涉及构建工具链配置环节确实容易出幺蛾子。现在Vue社区的主流选择是Vitest它和Vite天然一家速度快配置简单。步骤也不复杂安装依赖npm create vuelatest my-project cd my-project npm install npm install -D vitest vue/test-utils happy-dom在package.json里增加脚本{ scripts: { test: vitest, test:run: vitest run } }如果用的是Vite一般不需要额外配置Vitest会读取vite.config的路径别名。但为了保险推荐在vite.config里加一段/// reference typesvitest / import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], test: { environment: happy-dom, }, })在src/components/__tests__/目录下写测试文件。这里最容易遇到的坑是环境缺失。Vue组件里经常用到localStorage、window、document这些浏览器环境变量Node环境里没有。解决办法就是指定测试环境为happy-dom或jsdom这样组件挂载和DOM操作才不会报错。另一个高频报错是组件里引入了CSS或图片资源Vitest解析时容易卡壳。解决方案是在配置里加test: { css: true, }如果还报错可以考虑把资源mock掉。配置跑通之后日常写Vue组件测试的体验其实非常顺尤其配合vue/test-utils去挂载组件、触发事件、断言渲染结果。3.5 高覆盖率是不是越高越好行业内经常用“代码覆盖率”作为写测试的KPI。覆盖率确实重要它能帮你发现哪些代码没被测试到。但我必须泼一盆冷水覆盖率数字好看不代表测试质量好。有一种“覆盖率至上”的团队写测试的目标是凑覆盖率于是专门挑简单的、不影响业务逻辑的代码来测而对核心复杂逻辑视而不见。最终覆盖率报表漂亮得不行实际保护力约等于零。正确的做法是覆盖率当参考不当地板。优先把核心模块的测试补到位再回头看覆盖率报告把明显空洞的地方补起来。对于像“错误分支处理”“极端输入”这类代码即使覆盖率拉满也值得专门补用例。4. 实际踩坑记录与常见问题排查4.1 Vue Vitest 常见报错排查把我在实际配置中遇到过的几个高频报错整理成了表格方便对照着排查报错信息原因解决方案Cannot find module ./App.vue配置里缺少Vue插件vite.config里加plugins: [vue()]ReferenceError: window is not defined测试环境是node不是浏览器环境在test配置里指定environment: happy-domCannot use import statement outside a module依赖第三方包是ESM格式没被编译test.server.deps.inline里把包加进去SyntaxError: Unexpected token CSS或资源文件被js解析开启test.css: true或者mock掉静态资源Failed to resolve import xxx路径别名没被测试工具识别在vite.config的resolve.alias里配置同项目一致这些报错本质都是“测试环境不完全等于浏览器环境”导致的多做一步环境适配就平了。4.2 测试跑挂的常见原因单元测试跑挂无非三大类断言值对不上、环境问题、异步时机没对。断言对不上最常见特别是浮点数计算0.1 0.2 ! 0.3的经典问题会在测试里频繁爆出来。处理方式很简单用toBeCloseTo代替toBeexpect(calcPayAmount(120, true)).toBeCloseTo(95, 2);异步时机问题主要发生在测试里有定时器、请求、事件循环的时候。比如测一个防抖函数没等定时器执行完就断言了必然挂。可以引入fake timersvi.useFakeTimers(); // ... 触发逻辑 vi.runAllTimers(); expect(fn).toHaveBeenCalledTimes(1);4.3 一个从“测试不可能”到“测试香”的真实改造分享一个真实的经验。之前有个老项目里面有个订单金额计算函数四百多行里层套外层看得人头皮发麻。起初团队所有人一致认定“这玩意儿没法测”。后来我们还是想办法测了先把函数里的纯逻辑部分抽离出来参数通过对象传入状态不再散落在全局改造成纯函数后写测试变得非常轻松。这个改造过程本身就是一次重构而驱动重构的正是“我要给它写测试”这个动机。很多时候一个模块写得越难测越说明设计有问题。拿“难测”当借口不写测试其实是把问题的症状当成了不治疗的依据。5. 基于LLM的单元测试新工具能解决老问题吗5.1 AI生成测试用例的现状“基于llm的单元测试”在最近是个高频词不少团队已经在用大模型辅助生成测试用例。从实际效果来看大模型在生成“补充性测试用例”方面确实有实用价值你给它一段函数它唰唰给你写十几个用例边界覆盖比你好手写来得全。但要说全自动不review那也是不现实的。我的使用方式是用LLM生成初稿一筛是否有语法错误二筛是否有逻辑错判三调整风格。它帮我省了八成的时间剩下的两成需要人来把关。这才是合理的定位——辅助工具不是替代者。5.2 一个实操示例让LLM帮忙写测试假设有这样一个函数负责判断不同用户等级能享受的折扣率function getDiscountRate(userLevel) { switch(userLevel) { case normal: return 0; case silver: return 0.05; case gold: return 0.1; case platinum: return 0.2; default: return 0; } }我给LLM的提示词大概这样写请为下面的函数生成单元测试覆盖所有分支、边界值、未知输入、大小写不一致、空值等情况。函数如下 function getDiscountRate(userLevel) { ... } 要求使用Vitest风格的test()写法。它生成的测试里大概率会有以下这些有价值的场景每个等级各一个默认返回0传入null或undefined传入未知等级传大写字符串甚至传入数字类型。这些覆盖点如果人肉去列倒也能列出来但AI给你列好了你只需要确认一下“嗯这些都对”就行。5.3 AI生成测试的注意事项与局限要留意几个点。第一AI生成的断言不能全信它基于对你代码语义的理解而如果代码本身行为就是有bug的比如打折逻辑算反了它的断言大概率也按bug的行为去断言相当于“给bug背书”。所以理想情况下你得先确认函数期望行为再让AI生成测试或者是生成后自己逐一核对断言值对不对。第二大模型容易在“逻辑复杂”的函数上产生幻觉尤其是涉及状态变化、多个对象交互的代码生成出来的测试经常是“看起来对跑起来挂”。这种场景建议只让它生成脚手架核心断言还是人来写。第三测试代码的可读性也是一道门槛。AI生成的变量名、测试描述往往比较生硬保留关键断言同时微调命名维护起来才不费劲。6. 落地建议怎么把单元测试变成团队习惯6.1 从最小可行测试开始如果是新项目一开始就配上测试是最好的。如果是老项目不要幻想一步到位那样推进不了两天就会因为“改动量太大”半途而废。落地策略应该是日拱一卒。具体做法是每次修改某个模块顺手把这个模块里涉及到的核心逻辑测一测。修bug的时候先写一个复现这个bug的测试再修代码修完测试变绿bug就“永久”记录在案了。这有一个专门的说法叫“测试驱动bug修复”听着高端其实非常朴素让测试跟着你的修改走而不是专门腾日子补测试。6.2 写测试的优先级怎么排序给团队立一条规矩凡是被两个以上地方调用的函数必须有测试。这是最划算的起点。再往上凡是涉及钱的逻辑金额、优惠、汇率、时间的逻辑日期、时间窗口、时区、状态的逻辑状态机、权限、流转条件这三类无脑排优先级。这些位置一旦出错损失直接且难以挽回。UI层和胶水层可以后置除非你已经为复杂交互头疼过。6.3 关于测试的测试不要让测试成为新的维护负担最后啰嗦一句测试代码也是代码同样会腐烂。一段时间后有些旧用例会变成“永远在跑但没人理解”的摆设。这个时候不要心疼该删就删。一个好的测试文件维护成本应该极低。如果发现你每次改功能都要跟着改测试、且改的是断言值本身这时候要警觉——大概率不是测试错了而是业务行为变了你没发现。写测试的人应该具备的敏感度是测试不是加在功能之上的累赘而是功能设计的一部分。单元测试这事本质上跟养车差不多平时定时保养觉得费钱费事等真在半路抛锚了才发现花的最值的就是那笔保养费。写测试带来的那份踏实感是在无数个黑夜里拯救过我的东西。希望这篇聊完你能把“单元测试有什么用”这个问题从心里翻篇。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →