ARTICLE DETAIL

资讯详情

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

单元测试实战指南:从TestNG到Vue,构建可靠代码防线

单元测试实战指南:从TestNG到Vue,构建可靠代码防线 说实话早年听到“单元测试”这四个字我内心是有点抗拒的。当时觉得代码能跑起来就不错了写一堆测试用例不是浪费时间吗直到后来被线上故障按在地上摩擦过几次才彻底扭转了这个观念。现在我的团队里新功能没有单元测试保护根本不敢往主干上合。这个内容就是想把我在后端TestNG集成、前端Vue测试报错排查以及测试标准落地这些实战里踩过的坑、悟出的道理一次性掰开揉碎讲清楚。不管你是在校学生、刚转行的新人还是已经被测试债务拖累的团队主力这篇内容都值得你花十分钟认真看一遍。咱们废话不多说直接进入正题。整篇文章会从“为什么需要单元测试”这个根本问题入手然后依次拆解后端TestNG的实战集成、前端Vue测试的疑难杂症最后给出一个能直接抄作业的测试标准清单。1. 单元测试的本质与价值重构1.1 我们为什么必须正视单元测试如果你觉得单元测试只是“验证一下方法有没有报错”那说明你对它的理解还停留在表面。单元测试真正的价值不是证明代码现在是好的而是确保它在未来的每一次修改后仍然是对的。我用一个生活化的类比来解释单元测试就像你家里装修时埋的水管试压。你不可能等全部装修完成、家具进场之后才发现主水管漏水那代价是不可接受的。你必须在每一段管道接好之后立刻通水试压。单元测试干的就是这件事它在每一段代码逻辑“接好”之后立刻验证这段逻辑是否满足预期。很多团队的代码质量差并不是开发者水平低而是缺了一套“试压机制”。没有单元测试每一次代码重构都像是在雷区里行走你不知道哪次修改会把深埋的bug引爆。有了单元测试你就有了一个自动化的安全网跑一次测试就知道哪里被炸坏了。这正是单元测试在整个软件工程体系中最底层的价值——可持续维护的自信。从技术人的职场角度看单元测试也是一种隐形的竞争力。我面试过不少候选人简历上写着“熟悉单元测试”但一问到“你如何保证测试的有效性”就哑火了。这说明大部分人只停留在“会调用一个断言方法”的层面完全没有构建系统化测试体系的能力。而这个能力恰恰是中级工程师向高级工程师进阶的关键分水岭。1.2 单元测试在整个测试金字塔中的定位在讲TestNG和Vue测试之前我们必须先搞清楚单元测试处于什么位置这样才知道界线在哪里。自动化测试体系中有一个经典的“金字塔模型”从底到顶依次是单元测试针对函数、方法、类等最小单元进行验证。速度快、执行稳定、定位精确。一个中大型项目单元测试的数量应该是几百上千个运行时长控制在几分钟内。集成测试验证多个模块、多个组件之间的协作是否顺畅比如后端服务与数据库的交互、前端组件与页面状态的联动。端到端测试从用户视角模拟真实操作流程验证整个系统的完整行为。这类测试最接近真实场景但速度最慢、稳定性最差、维护成本最高。说白了端到端测试是照着地图走路走错了再回头看哪里错了而单元测试是你走每一步之前先低头看看这一步踩不踩得稳。一个好项目应该用大量的单元测试兜住地基用少量关键路径的端到端测试确认整体流程。如果反过来只写端到端测试每次跑测试都得等半小时几天就要修一次脚本这种测试反而是团队的负担。1.3 到底什么样的代码才算“能被测试”这是个很现实的问题。我在代码评审中见过太多写不动的测试测试一个方法要初始化数据库连接、要启动Redis、还要Mock好几个第三方服务……这说明被测代码本身设计得不够好。可测试性是现代代码质量的一个重要指标。它要求你在写业务代码时就把依赖关系显式化。举个例子一个订单服务的方法里直接商户系统写死一个静态配置这个方法就是不可测试的。如果你把第三方调用封装成一个接口传入测试时传入一个假实现这个方法就立刻变得可测试了。所以很多时候我们要用控制反转和依赖注入的思想来组织代码这不仅仅是架构上的傲慢也是为了让代码能在测试环境里被“孤立”出来运行。这里我特别想强调一个原则单元测试的“单元”不是一个“方法”不也是一个“对象”而是一条独立的行为路径。只要你能把这条行为路径从外部依赖中隔离出来速度足够快、结果足够确定那它就是一个合格的单测单元。2. 后端实战项目集成TestNG的那些门道2.1 为什么我从JUnit转向了TestNG提到Java后端的单测框架很多人第一时间想到JUnit。确实JUnit的使用率极高Spring Boot官方文档也是以JUnit为主。但在我近几年的项目实战中特别是接手一些需要数据驱动和复杂依赖控制的中大型系统后我慢慢倾向于使用TestNG。这不是说JUnit不好而是TestNG在某些场景下确实更顺手。先说TestNG的几个比较明显的优势更灵活的注解体系TestNG的注解命名直观、划分清晰比如BeforeMethod、AfterMethod、BeforeClass、AfterClass你很容易理解在什么阶段执行什么初始化逻辑。强大的数据驱动能力DataProvider是TestNG的王牌功能。它可以直接把一个数据提供者方法关联到测试方法上实现一套逻辑、多组数据、全部验证的效果。这对接口入参边界测试、权限分支测试来说极其好用。支持依赖测试有时我们需要严格指定某个测试方法在另一个测试方法成功之后才执行比如“登录方法成功”是“获取用户信息方法”的前置依赖。TestNG通过dependsOnMethods或dependsOnGroups天然支持这种场景。测试组Group机制你可以把测试分为“冒烟”、“回归”、“全量”等不同级别执行时按需选择运行哪些组。这在持续集成流水线里非常实用比如提交代码后只跑“P0级别”的快速验证夜间构建再跑全量。JUnit 5虽然也吸收了TestNG的很多特性但TestNG在复杂业务场景下的灵活性和稳定性仍然让我保持着使用习惯。如果你的项目是全新的、团队也愿意接受新规范选JUnit 5也没问题但如果你是维护老项目需要快速补充测试、或者需要数据驱动和依赖控制TestNG是非常稳的选择。2.2 从零到一并入MavenTestNG环境搭建详解这部分我直接把可复用的步骤写出来大家照着操作就行。第一步在pom.xml中引入测试依赖如果你用的是Maven需要在pom.xml里添加以下内容dependency groupIdorg.testng/groupId artifactIdtestng/artifactId version7.8.0/version scopetest/scope /dependency如果你用的是Gradle则在build.gradle中加testImplementation org.testng:testng:7.8.0版本号我建议用当前较新的稳定版。需要注意TestNG 7.x系列要求JDK 8及以上如果你的服务还在用JDK 7请升级或者使用旧版TestNG 6.x。第二步让Maven Surefire插件认识TestNGMaven默认会用Surefire插件来跑测试。如果你只是把TestNG加入了依赖却不告诉Surefire它可能仍然按JUnit的方式去找测试结果就是测试类一个都没执行。解决方法是在pom.xml的build节点里显式指定依赖plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId version3.1.2/version configuration suiteXmlFiles suiteXmlFilesrc/test/resources/testng.xml/suiteXmlFile /suiteXmlFiles /configuration /plugin这一句配置很关键它告诉Maven执行测试时请读取testng.xml这个套件文件。套件文件里定义了本次测试要跑哪些类、哪些组、启用哪些监听器等。第三步编写测试套件文件testng.xml这是一个最基础但很完整的套件文件!DOCTYPE suite SYSTEM https://testng.org/testng-1.0.dtd suite nameAll Tests test nameUnit Tests groups run include nameunit / exclude nameintegration / /run /groups packages package namecom.example.service / package namecom.example.util / /packages /test /suite这个配置的意思是本次运行只跑unit分组、排除integration分组并扫描指定包下的所有测试类。这个思路在大型项目里是很好的实践你不可能每次提交代码都等所有测试跑完把耗时长的集成测试或外部依赖测试放到夜间构建里才是合理的节奏。第四步编写第一个TestNG测试类这里我写一个比较贴近真实业务的示例public class UserServiceTest { private UserService userService; BeforeClass public void init() { // 这里可以初始化测试数据、准备Mock对象 userService new UserService(new InMemoryUserRepository()); } DataProvider(name invalidUsernames) public Object[][] invalidUsernames() { return new Object[][] { { }, { null }, { abc }, // 长度不足 { a#bc12345 } // 包含非法字符 }; } Test(dataProvider invalidUsernames, groups { unit, p0 }) public void createUser_withInvalidUsername_shouldThrowException(String username) { Exception ex Assert.expectThrows(IllegalArgumentException.class, () - userService.createUser(username, password123)); Assert.assertEquals(ex.getMessage(), 用户名不合法); } Test(groups { unit, p0 }) public void createUser_withValidParams_shouldReturnUserId() { long userId userService.createUser(leohappy, password123); Assert.assertTrue(userId 0); } }这段代码里有几个值得注意的细节使用DataProvider提供了四组非法用户名数据一套断言逻辑就完成了多种异常场景的覆盖用Assert.expectThrows断言方法必须抛出指定异常分组里同时打了unit和p0这样在流水线里能灵活选择。真实工作里光一个createUser方法就能写出七八个这种测试处理各种边界。2.3 Mockito与TestNG的融合技巧只测自己写的逻辑不测外部依赖这是老生常谈。但怎么把外部依赖“挡”在测试外面非常讲究技巧。在我的项目里TestNG和Mockito的组合使用率接近百分之百。我们在pom.xml里加入Mockito依赖dependency groupIdorg.mockito/groupId artifactIdmockito-core/artifactId version5.6.0/version scopetest/scope /dependency然后在测试类里这样使用public class OrderServiceTest { Mock private InventoryClient inventoryClient; Mock private PaymentClient paymentClient; InjectMocks private OrderService orderService; BeforeMethod public void setUp() { MockitoAnnotations.openMocks(this); } Test(groups { unit }) public void createOrder_whenInventoryUnavailable_shouldFail() { when(inventoryClient.isAvailable(SKU001, 1)).thenReturn(false); boolean created orderService.createOrder(SKU001, 1); Assert.assertFalse(created); verify(paymentClient, never()).pay(anyDouble()); } }这里有两个很关键的实战经验BeforeMethod比BeforeTest更适合做Mock初始化。因为BeforeMethod每个测试方法运行前都会执行保证每个用例从完全干净的Mock环境开始避免用例之间的状态污染。verify(paymentClient, never()).pay(...)这行很重要它不只验证结果正确还验证了流程没有被错误执行。好的单元测试应该既管“结果”又管“过程”。我想强调一个很多新手容易犯的错Mock不是越多越好。如果发现测试里要Mock五六个依赖那八成是被测类职责太重了。这时候不要硬写测试反过头去重构被测类让它依赖更少、边界更清晰才是正道。3. 前端破局Vue项目单元测试的疑难杂症实录3.1 前端测试基建怎么选Jest与Vue Test Utils组合很多后端开发者转型做前端后第一个困扰就是前端测试到底用什么工具。Vue生态里目前最主流的组合就是Jest Vue Test Utils。Jest是Facebook出品的测试框架它以零配置、自带断言、自带Mock和覆盖率统计而著称Vue Test Utils则是官方提供的组件测试工具库专门用来挂载组件、模拟交互、查DOM结构。这个组合选型有非常现实的理由Jest对原生ES Module、TypeScript以及Vue单文件组件的支持非常顺利不需要像摩卡那样做大量胶水配置。Vue Test Utils的API是官方维护的与Vue版本保持同步用它测试Vue组件是名正言顺的标准路径。Jest自带快照测试功能对UI组件的结构变化能做快速校验。在创建Vue 3项目之前大家可以用npm create vuelatest来初始化或者直接vue create也行。关键是在选功能时勾选Unit Testing并选Jest作为测试运行器。这里我特别提醒一句Vue 3的测试基建和Vue 2时代差别很大网上很多教程还停留在vue/cli-plugin-unit-jest配合Vue 2的写法照抄大概率会掉坑。我的建议是无论你用的是Vue 2还是Vue 3先查一下当前项目官方插件的最新主版本别用那种上古教程里的老配置。3.2 最常见的报错现场moduleNameMapper配置引发的惨案热搜词里把“vue单元测试报错”顶了上来这绝对是真实痛点。我当初在Vue项目接Jest时踩得最深的一个坑就是别名解析问题。项目里通常会用指代src目录比如import { formatDate } from /utils/dateJest在跑测试时是运行在Node环境里的它不知道你在Vite或Webpack里配置的别名是什么意思。不配置的话就会出现经典的报错Cannot find module /utils/date from src/components/HelloWorld.spec.js解决方式是在jest.config.js里加moduleNameMappermodule.exports { preset: vue/cli-plugin-unit-jest, transform: { ^.\\.vue$: vue/vue3-jest, ^.\\.jsx?$: babel-jest }, moduleNameMapper: { ^/(.*)$: rootDir/src/$1 }, testEnvironment: jsdom, testMatch: [ rootDir/src/**/*.spec.js, rootDir/src/**/*.test.js ] }这个配置里moduleNameMapper的作用就是告诉Jest所有以/开头的模块都去src目录下找。这个配置我建议所有的Vue单测项目必写它直接决定了你的测试能不能引用到业务代码。3.3 React组件挂载与异步更新难题还有一个高频报错出现在我们测试一个异步组件的时候。假设有个组件在onMounted里请求接口template div classuser-info p{{ userName }}/p /div /template script setup import { ref, onMounted } from vue import { fetchUser } from /api/user const userName ref() onMounted(async () { const res await fetchUser(1) userName.value res.name }) /script如果测试里直接断言十有八九会失败因为异步请求还没回来userName还是空字符串。你需要在Wtils中挂载组件后等待微任务队列清空import { mount, flushPromises } from vue/test-utils import UserInfo from /components/UserInfo.vue jest.mock(/api/user, () ({ fetchUser: jest.fn().mockResolvedValue({ name: leohappy }) })) describe(UserInfo, () { test(renders user name after fetch, async () { const wrapper mount(UserInfo) await flushPromises() expect(wrapper.find(.user-info p).text()).toBe(leohappy) }) })很多新手被这种报错卡住核心原因是没理解组件渲染和断言之间存在异步间隙。Jest测试不是说是异步的吗No得等数据更新完成后再去断言。这里flushPromises就是用来把Promise队列排干让Vue内部的异步渲染动作完成的。如果你的组件里有用到nextTick、setTimeout、requestAnimationFrame这些还需要结合vi.useFakeTimers()和手动advanceTimersByTime来模拟时间推进。这种问题没有现成通解按报错提示一个一个来。3.4 组件渲染出现“找不到client但测试run不了”的怪象这个报错我在Vue 3 Vite项目里遇到不止一次[Vue warn]: Failed to resolve component: router-link原因是组件里用了router-link、RouterView或者useRouter但测试时只挂载了组件本身没有给它一个Router上下文。解决办法有两种。方式一引入真实Router并设置historyimport { createRouter, createWebHistory } from vue-router import routes from /router const router createRouter({ history: createWebHistory(), routes }) const wrapper mount(App, { global: { plugins: [router] } })方式二更轻量的做法是创建Stub很多单元测试其实不关心路由跳转是否正确只关心页面渲染是否正常。这时可以直接替换掉路由相关组件const wrapper mount(App, { global: { stubs: [router-link, router-view] } })第二种方式在纯组件测试里更干净减去了Router的额外负担。在方案选型上我的经验是测试组件自身逻辑用Stub测试路由联动行为用真实Router两者目的不同。3.5 Vue3 TypeScript项目的特殊注意点如果你的Vue项目用了TypeScript那么Jest的配置还需要再做一下转型。推荐用ts-jest来直接处理TypeScript文件module.exports { preset: ts-jest, testEnvironment: jsdom, transform: { ^.\\.vue$: vue/vue3-jest, ^.\\.tsx?$: ts-jest }, moduleNameMapper: { ^/(.*)$: rootDir/src/$1 } }然后安装对应依赖npm install -D jest ts-jest types/jest vue/vue3-jest vue/test-utils这里有个容易被忽略的问题如果你的tsconfig.json里设置了noImplicitAny: true那么测试文件里会出现很多“隐式any”的报错。我的建议是测试代码可以单独放置一个tsconfig.jest.json放宽类型检查专注跑通用例等业务环境执行严格检查。测试代码的类型边界不需要跟生产代码一样苛刻但也不能完全放飞自我至少保证测试逻辑本身是对的。4. 单元测试标准的落地化实践4.1 到底什么样的覆盖率才算“达标”这是每个团队落地单测时必然吵起来的问题。“行覆盖率必须到80%”这种口号喊出来容易真正执行起来很多人就天天为覆盖率数字而战反而忘了测试的本质。我就见过一个团队为了让行覆盖率达标写了大量“防御性测试”——只是为了覆盖某一个if分支而硬造数据完全没断言业务逻辑是否正确。这种测试除了让覆盖率数字好看一点对质量提升几乎毫无帮助还白白增加了维护成本。我倾向于把覆盖率分成几个层级来要求核心业务模块行覆盖率不低于80%分支覆盖率不低于70%。比如订单支付、库存扣减、权限控制这些模块它们出bug的代价极高值得重点保护。普通业务模块行覆盖率不低于60%。比如偏CRUD的服务只要主干流程被验证即可细枝末节的校验逻辑可以挑选关键的写。工具类与基础设施行覆盖率不低于85%。因为工具类是全局的基础依赖一个日期格式化函数可能在无数个地方被调用必须高度可靠。但这不代表覆盖率是万能的。我认为比覆盖率更重要的是关键行为的覆盖度核心异常分支有没有被验证关键状态流转有没有被测试第三方回调是否被断言这些属于“设计层面”的覆盖就算行覆盖率低了点但关键玩法都测到了这个测试体系依然是有效的。4.2 一个可复用的测试用例设计标准我整理了一套写用例的标准模板团队新同事照着这套模板写出来的测试质量基本不会太差。标准共有四个维度第一个维度入参边界与异常每个对外方法必须至少覆盖三类入参正常合法值、边界值最小值、最大值、空值以及非法值。比如一个转账方法金额参数的非法值必须包括负数、0、超过余额数这些场景。每个非法值都应当用expectThrows明确断言异常类型和错误信息。第二个维度分支与条件全遍历代码里if-else、switch-case带来的不同执行路径必须有不同用例覆盖到。可以用代码覆盖率工具来辅助判断跑完测试后看哪些行没有被执行到逐个分析是漏测还是死代码。第三个维度外部依赖的成功与失败轨迹与数据库、缓存、第三方服务的交互必须分别Mock成功、失败、超时三种结果断言返回值和副作用是否符合预期。只测成功轨迹的单元测试等于只给水管道试压了正常水流却没有测试爆管场景。第四个维度幂等性与状态恢复多测试用例运行后系统应当恢复到初始状态。具体到代码里就是测试数据库里的数据不能被污染Mock对象的调用次数不能被记到下个用例头上。这一条执行得好的团队测试是能随便乱序执行的执行得差的团队删掉一个用例居然会引起另外五个用例报错。4.3 测试代码本身也是要Review的很多团队把Code Review的重点全放在业务代码上测试代码基本没人细看。我强烈建议改变这个习惯因为测试代码同样是项目的资产它被维护的时间甚至比业务代码更长。Review测试代码时我会重点关注这几个点测试用例有没有命名清晰、语义完整。test_createUser_shouldThrowExceptionWhenUsernameIsEmpty就比testBadUser清晰得多因为前者一眼能看出验证什么行为。是否只验证了外部可观察的行为而没纠缠被测对象内部实现细节。断言内部私有方法被调用通常意味着测试与实现过度耦合重构时会碎一地。有没有做完整的状态清理。比如为RedisMock或数据库Mock准备的数据是否在AfterMethod里清除不然就会产生局部性的状态污染。断言是否充分。只是“没有抛异常”不等于结果正确必须检查方法的返回对象、副作用、以及相关依赖是否被按预期调用。换句话说Review测试代码时你要像审查一份规范合同一样对待它。这份合同约束的是你代码的行为底线契约写得含糊不清后面维护的人就会开始想办法“跳过合同”。4.4 我见过的“高风险但零测试”代码场景还有些场景属于高危地带容易影响线上逻辑正确性但单测却常常缺位我单独拿出来提醒一下。第一类是时间相关逻辑。比如“当前时间是否在活动有效期”、“订单是否超过48小时未支付”。这类逻辑如果依赖new Date()获取的真实时间测试根本没法覆盖未来和过去场景。我建议你封装一个Clock接口生产环境返回真实时间测试环境注入固定时间。这个改造的成本极低却能瞬间打通时间逻辑的可测试性。第二类是分布式锁、幂等控制。这类代码涉及并发和重试不写单测隐患极大。比如“同一订单同时提交两次只能成功一次”这种场景在没有真实并发环境时单测里可以用两个线程模拟并发请求只要实现够快还是能发现部分问题的。第三类是数据转换/序列化逻辑。比如把前端传入的A协议对象转成内部B模型这种转换代码最容易出现字段漏掉、类型转错等问题。这种场景必须写测试用真实结构的对象输入断言输出对象的每个字段都正确。第四类是异常降级逻辑。比如第三方短信发送失败时是否降级为日志记录不阻塞主流程主Redis挂了是否切到备用Redis。这些代码平时根本不会触发但它们一旦触发就是线上故障。不用单元测试把降级路径跑一遍等于把备用胎放在后备箱里却从没检查过它有没有气。这些高危场景通常不是团队故意不测而是大家根本没有意识到“这东西居然还可以测”。一旦你突破了“测试就是调用方法看返回值”的思维限制你会发现几乎所有逻辑都能找到方式去验证。5. 常见问题排查与实用技巧速查这一节我直接把平时排查测试问题的高频思路整理成速查表大家遇到问题时按图索骥能解决绝大部分麻烦。症状可能原因排查思路Cannot find module /xxxJest没配置路径别名检查moduleNameMapper是否指向正确目录测试跑完但总显示运行0个测试Surefire没识别TestNG套件检查pom.xml的suiteXmlFiles配置是否正确用例之间互相影响、乱序执行失败有共享状态没清理在AfterMethod或afterEach中加状态重置逻辑Mock对象的when配置不生效Mock对象没用对实例确认被测类被InjectMocks注入的确实是同一个MockVue组件找不到router-link组件依赖Router上下文用stubs或global.plugins注入Router异步渲染后断言失败断言前没有等待更新完成使用await flushPromises()或nextTick()时间相关方法不好测直接用了new Date()封装Clock接口并在测试中注入固定时间覆盖率数字一直不涨测试没跑到目标代码打开覆盖率报告逐行看未覆盖分支补充针对性用例5.1 TestNG的两个小众但能救命的注解BeforeMethod和AfterMethod大家基本天天用但有几个冷门注解是不少人没用过的。一个是BeforeSuite和AfterSuite它们控制的是整个套件级别的执行时机非常适合用来初始化一次性的重量级资源比如启动一个内嵌的H2数据库、初始化一个连接池。要注意这种全局资源一旦被污染整个套件的测试都会失败所以清理工作必须做得非常干净。另一个是Factory它可以从一个数据集合生成一批测试类实例。当你的测试需要根据不同的商家配置跑同一套流程时Factory会让你优雅很多它创建的每一个实例都是独立的测试对象互不干扰。5.2 前端测试中Mock全局插件与CSS模块的技巧Vue组件里往往还有Element Plus、Vant等UI库以及window.scrollTo、matchMedia这类浏览器环境对象。在测试中全局插件一旦加载整个测试环境就会出现“undefined is not a function”这类报错。我的处理方式是在jest.setup.js里统一为全局浏览器API打补丁// jest.setup.js window.matchMedia window.matchMedia || function () { return { matches: false, addListener: function () {}, removeListener: function () {} } } global.ResizeObserver class { observe() {} unobserve() {} disconnect() {} }然后在jest.config.js里把这个setup文件引进去module.exports { setupFiles: [rootDir/jest.setup.js] // ...其他配置 }这样Centralizes所有补丁逻辑各测试文件里不用反复写这些胶水代码。5.3 测试金字塔之外的“逆向思维”最后分享一个让我受益很大的习惯写完一个bug修复后不要急着提交先写一个重现这个bug的单元测试跑一遍确认它真的会失败然后再去修改业务代码。等业务代码改完后再跑这个测试如果通过了代码就修好了如果没通过说明修复还没到位。这个流程叫“红-绿-重构”循环它的核心意义在于让测试成为bug修复的准星而不是马后炮。这个方法听起来很简单但坚持执行会极大地提升修复质量。很多人修bug是“大概改一下看起来没问题了就交差”结果过几天同一个bug的变种又冒出来了。如果你先写了失败测试就从根本上断了这种侥幸的路径。5.4 测试和代码重构的配合节奏实际开发里还有一个高频诉求我要重构一段代码但又怕改出问题。我的建议是重构前先看这段代码有没有有效的单元测试。如果没有第一件事不是重构而是先给旧行为“拍一张照片”——把现有行为用测试固定下来。等重构完成再跑一遍这些测试只要全绿行为就基本没有变化。这里说的“固定行为”不要求测试很优雅最好只针对入口和出口做断言不要耦合内部实现的先后顺序。比如把一个循环合并到一个方法里测试只关心输入一批订单后输出的总金额是不是对的而不关心你内部是先排序还是后累加。这样重构时你就有了一张安全网误伤行为的概率大幅降低。6. 尾声与个人心得这篇文章从为什么写单测、后端TestNG集成、前端Vue测试的坑一直聊到测试标准的设计和常见的排查技巧。对我来说单元测试最大的魅力不在于它能让你的代码更“漂亮”也不在于它可以作为简历上的一个技能点而在于它给了开发者内心的一种笃定感——你合代码的那一下知道自己不会把线上的路给拆了。我特别想对还在犹豫要不要引入单元测试、或者已经开始写但坚持不下去的读者说一句刚开始写单测一定会觉得慢一个简单方法恨不得写上半天测试代码太打击人了。我的建议是不要试图一步到位也不要指望一个月内把整个系统测完。你可以挑一块价值最高、最容易出bug的模块入手比如支付、权限、消息解析集中力量把它测透。等你尝到了“改完代码、一键跑完几百个测试、全绿”的那种安全感之后你会主动想把全项目都纳入单测保护伞之下。最后再分享一个小技巧吧。如果你在本地跑大项目全量单测总是很慢几乎没人愿意等。我的习惯是给测试用例打上分级标记提交代码和本地开发时只跑P0级别的快速用例夜间流水线再跑全量回归。这就像打仗时分梯队先头部队解决眼前的敌人主力部队负责全面清场次序对了效率自然上来了。这套思路比逼着每一个人用意志力去忍受漫长等待要可靠得多。
返回列表