单元测试作为提升代码质量的有效方法,目前在国内各大互联网公司的开发团队中,尤其是业务团队中却鲜少被使用。这主要由于大家对于单元测试有一些认知错误,或者没有正确的打开方式。至今我们团队在小剧场、零代码运营平台等项目都进行了一些单元测试的实践运用,单元测试对我们的代码质量及开发效率都大有裨益,故希望通过此篇分享我们在单元测试的一些实践经验,帮助大家更好的使用单元测试来为自己的系统质量保驾护航。
方法篇
为什么需要单元测试
单元测试的定义
一个单元测试是一段自动化的代码,这段代码调用被测试的工作单元,之后对这个单元的单个最终结果的某些假设进行检验。单元测试几乎都是用单元测试框架编写的。单元测试容易编写,能快速运行。单元测试可靠、可读,并且可维护。只要产品代码不发生变化,单元测试的结果是稳定的。
基于上方的单元测试定义,我进一步总结了一些单元测试的基本特征,只有满足以下所有特征才是合格的单元测试。
自动化检测
单元测试应该是无需开发手动进行验证的,运行单元测试应该自动进行结果的校验并输出不通过的单元测试用例。反例是通过结果的输出手动比对。
快速运行
单元测试应该是很快被执行的,一个项目所有的测试用例应该在10s内全部被执行验证,快速的单元测试才能在开发过程中反复检测。反例是单元测试依赖Spring IOC容器启动。
无依赖
单元测试不应该依赖任何外部资源,包括文件系统、数据库、RPC,所有对外资源的调用都应该采用MOCK的方式。
结果稳定
单元测试运行检测不应该依赖运行时刻的部署环境、时间、地理位置等外部信息。反例是一个错误的单元测试依赖了当前时间,当在未来运行时会因为时间的变动而导致结果不一致。
粒度小
单元测试检测的功能范围应该是小粒度的,这里会在后文做更详细的说明。
持续维护
单元测试非一次性的代码,他也需要随同业务代码的发展持续进行维护。不易维护、不可阅读的单元测试是无意义的,最终会成为系统代码中的累赘,最后也无人关心这些测试代码是否通过了。
单元测试与其他测试的区别
大家对单元测试的最常见误区之一就是将单元测试与集成测试概念混淆。单元测试与集成测试最大的区别在于单元测试不依赖任何外部资源,并且测试的范围只限定于单个功能单元,而集成测试会依赖外部资源,并且核心是将某一业务流程全部走通。系统测试是必须系统完整运行,并通过Http接口等进行链路验证,相较于集成测试它的编写及运行成本更高,检测范围更广。
测试类型 | 编写及运行成本 | 用例数量 |
---|---|---|
系统测试 (End to End Test) | 高 | 少 |
集成测试 (Integration Test) | 中 | 中 |
单元测试 (Unit Test) | 低成本,高效运行 | 多 |
单元测试的作用
质量及信心的提升
单元测试最核心的作用是对代码质量的提升,通过单元测试我们可以对系统内各个功能单元都建立完备的自动化测试。相较于测试同学黑盒模式的测试验证,单元测试有其天然的优势,可以在逻辑代码内直接进行检测验证,穷举所有必要的用例。将生产代码进行有效的单元测试覆盖,也会极大的提升开发对代码质量的信心。
活文档
维护良好,用例清晰的单元测试,天然就是一个可调用的活文档。一个功能逻辑内各自用例只需要通过运行单元测试即可一目了然。
良好解耦的尺度
对一些老的代码我们在补充单元测试时发现难以进行单元测试的编写,这是由于此处逻辑设计不够耦合,导致难以拆解独立测试。
测试友好的代码必然是解耦的,所以我们可以将单元测试作为方法耦合的尺度,若无法简单的进行单元测试,功能方法必然存在不合理的耦合。
重构保障
单元测试不仅可以保障本次需求的质量,良好单元测试的维护更是一种长期的投资,通过建立对各个逻辑的单元测试检测,会为各层逻辑覆盖检测,为系统重构提供质量保障。
关于单元测试的成本
大家容易对单元测试产生的另一个误区是单元测试的引入会增加不必要的成本,影响业务的快速上线。但从我们在小剧场等业务的实践中发现单元测试的使用,不仅提高了我们的质量也提升了我们的开发效率。
在一个业务需求中,开发时间一般只占比到百分之三十,其余为调试及测试时间。编写单元测试的确需要在开发阶段开销一定的时间成本,但是在编写单元测试时实际我就是在进行自测了,且单元测试调试快速,定位问题精准,无需部署调试。当我们对复杂的策略逻辑完成单元测试后,可以极大的缩短我们的联调和提测时间。从实践结果来看,由于更少的自测联调成本,采用单元测试反而提升了我们的开发效率。
另一方面单元测试具有长期的价值,这一份测试代码的存在,可以长久的守护逻辑的正确,当后续逻辑修改了之前的部分功能而使得老功能不符合预期时,单元测试可以快速定位到问题,很大程度的避免了修改老逻辑导致的问题。
如何写好单元测试
什么场景适合单元测试
若使用了单元测试,我们不必过于追求单元测试的覆盖率,并非所有代码都需要进行单元测试的覆盖,哪些地方适合单元测试我总结如下:
• 代码中存在运算逻辑的代码,以小剧场为例,剧的上下架运算逻辑、运营数据的通用过滤逻辑适合。
• 一些数据的简单到库的增删改查逻辑,不需要进行单元测试的覆盖,这些地方是比较简单的,覆盖意义无非是单纯的方法调用。
单元测试的粒度
前文中单元测试的基本特征中谈到,单元测试的粒度要小,这里的粒度指的是测试的边界范围要小,一个单元测试测试多个联动的业务功能是不合理的。我们以订单开单功能为例,一个业务的业务用例往往可以按树结构进行拆分为多层次的子功能。
一个订单的开单功能又涉及到商品的库存检测、促销的优惠检测、支付的价格计算等子功能,在对开单进行单元测试时,其核心需要测试的是开单功能对各个子功能结果的逻辑串联,不应该直接在此单元测试内向下进行调用,所有子功能的结果都应该按用例场景采用MOCK进行返回。
而所有子功能的正确应该由更下一级功能的单元测试自行保障,所以开单的单元测试编辑应该仅限于在开单流程层级,而不应该下渗到子功能的验证当中。
以此可以为系统功能建立多层次的单元测试体系,每一层次的功能都是经过检测的,并且也可以基于单元测试用例了解功能。
关于TDD
聊到单元测试,不得不谈到TDD,TDD全称Test-Driven Development 测试驱动开发,是一种不同于传统开发流程的开发方法,它要求在实现功能逻辑前,先进行单元测试的编写。因为有单元测试的提前介入,所以编写的代码必然是测试友好的。另一方面很重要的一点是使我们从调用视角来考虑顶层接口设计,自上而下的系统设计避免了自下而上的过度实现或实现与诉求不一致的情况。
TDD模式在我们团队并未进行广泛的使用,但在进行系统设计实现过程中我们充分的吸收了TDD自上而下的设计方式,进行代码功能开发的前期我们会先进行接口、模型的定义。
TDD的三定律
• 在编写不能通过的单元测试前,不可以编写生产代码
• 只可以编写刚好无法通过的单元测试,不能编译也算不通过
• 只能编写刚好通过当前失败测试的生产代码