软件开发实践:我的单元测试历程--这是一根不错的拐棍
最初,我对单元测试是抗拒的。
面对庞大的代码量和复杂的逻辑,
我常常怀疑:真的需要写那么多单元测试吗?代码覆盖率能达到90%甚至100%吗?
这似乎是一个不可能完成的任务。
更何况,如果代码重构了,之前的单元测试岂不是白写了?
这些问题让我对单元测试望而却步。
1. 最初对单元测试的思考
让我第一次认真思考单元测试,是在一次软件通过功能测试后,我计划重构代码时。
如何确保重构不会引入新问题?这时,单元测试进入了我的视野。
1.1 缘由
如果某个模块的接口(本文中的“接口”指外部可访问的函数或变量,变量不推荐)是明确的,并且有单元测试作为保障,那么重构模块内部代码时就会更加放心。
1.2 接口与覆盖率的思考
我逐渐意识到,接口是代码的边界。只要接口不变,边界内的代码可以随意修改。
同时,我也发现,单元测试的数量可能并没有想象中那么多。
如果接口是内部接口(不作为库使用),只要调用点覆盖到了,测试就足够了。
此外,单元测试不应过分追求代码覆盖率,而应注重逻辑覆盖率或功能覆盖率。
单元测试不应深入到代码细节,而应从接口的角度出发,考虑各种调用情况。
从这个角度看,单元测试更像是黑盒测试。
很多人被代码覆盖率搞得晕头转向,其实,只要代码逻辑清晰、整洁,覆盖率自然会提升。
后来我了解到,很多人认识单元测试的起点,都是从支持重构的角度出发的。
重构确实是一个重要的契机,它能引导我们思考单元测试的价值。
这里有一个常见的误区:很多人认为可以先编码,等到重构时再写单元测试。
这种观点往往源于没有真正实践过单元测试。实际上,等到功能测试完成后再重构,已经有些晚了。
1.3 单元测试与开发时间
我还发现,单元测试可能会缩短开发时间。
以Web测试为例(不包括页面修改),修改代码后需要编译、发布、打开网页、登录、定位测试点,甚至处理缓存问题。
这些步骤既繁琐又容易出错。
如果有单元测试,只需按一个键就能完成测试,效率大大提升。
在认识到单元测试的价值后,我开始有意尝试编写单元测试。
2. 对单元测试的探寻
2.1 第一次接触单元测试
刚开始工作时,我曾用JUnit写过一些单元测试。
但由于对单元测试理解不深,加上项目要求刻板(每个函数都要写伪代码),这次经历并没有让我积累多少有益的经验。
2.2 寻找测试框架
由于我主要使用C++,当时并不知道有GTest或NGGTest这样的框架。
或许这些框架还未出现,或者我的搜索方式有问题,总之我未能找到一个合适的框架。
为此,我浪费了不少时间。
后来,我在一个开源项目(可能是Live555)中看到,它的单元测试非常简单,直接使用printf
输出结果,甚至没有自动对比正确性。
这让我意识到,框架的形式并不重要,重要的是我们想要达到的目标。
于是,我开始自己编写简单的单元测试程序。
随着测试函数的增多,我逐渐演化出了一套自己的测试框架。
2.3 单元测试的边界
最初编写单元测试时,我常常跨越多层进行测试,甚至涉及通讯层(如ActiveMQ)和数据库交互。
这让我一度非常苦恼:如何测试这些第三方依赖?
后来我明白,第三方组件(如ActiveMQ和数据库)并不需要测试,我们只需假设它们是正确的。
我们的目标是测试自己的逻辑是否正确。
只要数据正确,其他部分可以默认是可靠的。
由于当时不了解Mock技术,我花了很多时间自己编写模拟函数来提供数据或进行判断。
3. 单元测试的一些思考
3.1 单元测试有利于降低代码的耦合度
我曾以为自己写代码时已经很谨慎,接口设计也很重视。
但在编写单元测试后,我发现仍有改进空间。
例如,有一次我编写了一个周期性运行的服务程序,函数中直接使用了系统时间。
这导致测试时无法控制时间,每次测试都需要等待。
后来,我将系统时间作为参数传入函数,测试变得方便多了。
虽然后来了解到可以使用Mock模拟系统时间,但我认为去除系统依赖是更好的做法。
3.2 单元测试与重构
我一直在思考,什么时候是重构的最佳时机?
功能测试完成后显然不合适,因为重构可能导致功能测试白费。
我曾认为,在完成几个类后,检查是否有共同点并进行重构是一个合适的时机。
或者,在完成一个类或源文件后,进行命名优化、注释添加和函数拆分。
但编写单元测试后,我发现,重构的最佳时机是在完成一个对外接口函数并编写单元测试时。
这时,可以通过单元测试验证接口设计是否合理、逻辑是否正确、分层是否清晰。
3.3 什么时候开始写单元测试?
TDD(测试驱动开发)提倡在编写代码之前先写单元测试,但我没有尝试过。
在编码时,我不想让单元测试打断我的思路,因此我选择在完成第一个对外接口函数后开始编写单元测试。
当然,也可以在编译时顺便编写单元测试。
但千万不要等到所有接口函数都完成后再开始,因为那时代码可能已经耦合度过高或分层不合理,难以测试。
3.4 单元测试代码量大吗?
很多人不写单元测试,是觉得测试代码量太大,性价比不高。
但实际上,单元测试的代码量并不大。
一个接口函数通常只需要四五个测试用例。
如果测试用例过多,可能需要重新审视函数设计。
我的经验是,测试函数通常不会太长,且同一接口函数的测试函数往往比较相似。
3.5 单元测试占用时间多吗?
很多人认为编写单元测试很耗时,但实际上并非如此。
-
单元测试的同时完成了重构,节省了重构时间。
-
单元测试能尽早发现问题,修复成本更低。
-
单元测试简化了调试过程,避免了重复搭建环境和执行步骤的麻烦。
-
单元测试可以快速验证代码效果,满足开发者的即时反馈需求。
3.6 单元测试难写吗?
单元测试并不难写。
我的思路是,用最少的代码完成测试功能。
编写单元测试就像写一篇有思路的作文,过程通常很顺畅。
3.7 Mock技术
Mock是我最近才了解到的技术,它非常有用。
以前,遇到外部接口调用时,我需要自己实现一个模拟版本。
有了Mock后,这一切变得简单多了。
Mock不仅可以模拟外部依赖,还能处理定时器、线程等复杂场景。
虽然Mock的语法稍复杂,但它的价值毋庸置疑。
我在实际工作中,会有意避免使用Mock。
3.8 单元测试在编码阶段的作用
我现在养成了一个习惯:
无论项目是否要求,我都会为新功能编写单元测试,即使这些测试不会提交到代码库中。
不写单元测试,就像登山没有拐杖,虽然也能完成,但总觉得不够方便。
编码完成后,我对单元测试的关注度会降低,这其实是一个不好的习惯。
单元测试在编码阶段的作用比重构时更大,因为在编码时,我已经完成了大部分的重构工作。
4. 单元测试就是一根“登山杖”
最后,我想说的是,单元测试很有用,但并不是必须的。
它是一种优秀的实践,但不必为此纠结。
如果你想尝试,建议立即开始,多做多思考,自然会体会到它的好处。
如果没有兴趣或没有想通,不写单元测试也无妨。毕竟,很多时候我们不写单元测试也能完成任务。只是每当遇到难以解决的Bug时,我总会感叹:“如果当初有单元测试,该多好啊!”
单元测试就像一根“登山杖”,虽然不是必需品,但在攀登代码高峰时,它能让你走得更稳、更远。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?