单元测试从入门到精通
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
1 前言
这篇文章源于工作中的一个项目,2021年,我负责汇川技术工业机器人应用软件的基础架构重构,当时单元测试是重构工作的核心环节之一,从无法进行单元测试到最终60%以上的行覆盖率,过程中自己也有非常多的收获,于是将其整理成文,希望对计划开展和正在开展单元测试的同学有所帮助。 2 什么是单元测试
单元测试(Unit Testing),是指对软件中的逻辑单元或组件进行检查和验证,以确保其按预期执行。通常单元测试是软件开发过程中进行的最低级别测试活动,通过单元测试可发现和修复软件开发早期的BUG和缺陷。 单元(Unit),是一个应用程序中最小的可测试部分,在面向过程开发中,单元通常为函数(Function),在面向对象开发中,单元通常为类中的方法(Method)。 3 为什么要进行单元测试
3.1 降低代码缺陷
单元测试的首要目标是降低代码缺陷,如上图所示,当代码缺陷越早被发现,它的修复成本就越低,这种把测试尽量提前进行的思想就叫做测试左移。 3.2 推动架构优化
单元测试与软件架构有着非常紧密的联系,通常越是架构设计优秀的项目(比如符合SOLID规则),越容易实施单元测试,反之越是架构糟糕的项目,越难以实施单元测试。并且单元测试以及其严格的方式要求软件架构设计,如果架构设计存在问题,单元测试就根本无法开展。 假如你发现在自己的项目中实施单元测试举步为艰,那么首先应该停下来观察和思考一下,项目架构设计的是否合理?比如一些项目中UI与业务逻辑耦合,单元测试无法命中核心业务逻辑,可思考一下项目是否需要分层设计?UI与业务逻辑是否需要分离?比如项目中当碰到物理设备的依赖,单元测试就被阻断,可思考一下是否永远只使用这一台设备?后续有没有可能换成其它设备?是否需要考虑扩展性?再比如发现被测对象就是铁板一块,根本不能改变其协作对象的行为和数据,那么就应思考一下,对象之间是否存在强耦合?是否可以通过依赖注入降低和消除对象之间的耦合? 大多数情况我们在项目中实施单元测试的目的是为了保障代码质量,但我认为单元测试对软件架构优化的驱动实际更为重要。 3.3 守护代码迭代质量
在当下的商业环境中,大鱼吃小鱼,快鱼吃慢鱼,对软件开发的效率要求越来越高,传统的瀑布开发模式越来越少,敏捷开发模式越来越普及。因此软件版本快速迭代,快速测试,快速发布,小步快跑在大多数项目中成为常态。 而单元测试在代码快速迭代过程中发挥着守护代码质量的至关重要作用。当单元测试覆盖了一个模块中的业务逻辑,该业务逻辑在迭代变更过程中出现任何问题,会第一时间自动被单元测试捕获,因此单元测试对发生变更的代码正确性提供了保障,同时开发人员在这样的保障下可以大胆的对代码进行重构,对业务逻辑进行增减变更调整。大名鼎鼎的TDD(测试驱动开发)就是基于这个原理。 4 如何进行单元测试
4.1 使用AAA规则编写测试用例
我们用一个简单的示例来演示单元测试的编码过程,如下代码所示,是一个非常简单的方法,它根据不同的距离,推荐不同的交通工具:
然后我们为这个方法编写单元测试用例,它同样非常的简单:设定条件,调用被测方法,断言返回结果:
至此,单元测试的编码就完成了。是的,单元测试的编码已全部完成了,花30秒看懂这个示例,你就掌握了单元测试的核心方法。为了方便记忆,有人将它总结成了AAA规则: Arrange
设置条件 Act
执行逻辑 Assert
断言结果 4.2 让每个测试用例符合AIR特性
AAA规则告诉我们如何编写单元测试用例,但要编写一个合格的单元测试用例,就需要了解单元测试用例的基本特性,这些特性就像空气(AIR)一样重要,任何时候我们也不能离开: Automatic
自动化:单元测试应自动执行,而无需任何交互,测试用例通常被定期执行。 Independent
独立性:每个单元测试用例都是独立的个体,不允许测试用例之间存在依赖关系,也不允许要求测试用例被执行的先后顺序。 Repeatable
可重复:单元测试用例在被重复执行时应稳定的返回相同的结果,不能受外部环境的影响。 4.3 在需要的时候使用测试替身
什么是测试替身
比如业务中我们的代码与硬件设备连接,需要依赖硬件的不同状态来执行不同的逻辑。单元测试的特性是随时可重复执行,对硬件的依赖会阻塞单元测试执行,因此在单元测试用例中需要用一个“替身”替换掉硬件的状态,这个“替身”就叫做测试替身。 测试替身的应用场景
1、真实对象具有不可确定的行为,或产生不可预测的结果。 2、真实对象很难被创建或创建成本过大,比如第三方系统、与硬件设备关联的模块。 3、真实对象的某些行为很难触发,比如异常的触发。 4、真实对象令测试用例的执行速度很慢。 5、真实对象有含有人机交互界面。 4.4 了解单元测试覆盖方式
语句覆盖
语句覆盖又称为行覆盖,是单元测试中最简单也是最常见的覆盖率统计方式。被测函数中,只要被单元测试用例执行到的行,即认为该行被覆盖到。比如一个100行的函数,其中有60行被单元测试用例执行到,那么语句覆盖率为60%。 分支覆盖
分支覆盖又称为判定覆盖,它关注的是被测函数中产生分支的if判定结果,只要每个if语句判定为真和判定为假的分支都被执行到,即达成了分支覆盖。注意分支覆盖并不考虑多个分支间的组合关系。 条件覆盖
条件覆盖关注的是判定语句中的每个表达式是否被执行。比如判定语句 if (a() || b()) ,当a()返回为真时就不再执行b()了,此时就未达成条件覆盖;要达成条件覆盖,就需要使a()返回假。 路径覆盖
路径覆盖是单元测试中覆盖最全的一种方式,它要求覆盖被测试方法中所有逻辑分支路径的组合。 总结
语句覆盖在单元测试覆盖率统计中最为常见,基本是一个必选项,分支覆盖与条件覆盖可作为进阶选择,路径覆盖最为完善,但是在复杂的业务场景中,会导致单元测试代码指数级增长。建议根据实际情况灵活组合搭配。 4.5 单元测试过程中的一些常见疑问
由谁来编写单元测试用例?
应该由开发人员编写自己开发的功能对应的单元测试用例。有些项目中会安排专人来为其它人开发的功能编写单元测试用例,这样做效率很低,因为单元测试用例的编写人员需要花费时间了解和学习代码逻辑。 什么时间节点写单元测试用例?
通常应该在功能开发完成后即编写与之对应的单元测试用例,即使有延迟也不要延迟太长时间,时间过长会导致编写单元测试用例时需要重新回顾代码逻辑所带来的额外时间成本。 单元测试是白盒还是黑盒测试?
绝大部分的单元测试是白盒测试,会根据函数中的逻辑设计编写测试用例,以达到覆盖率目标。但单元测试也可以是黑盒测试,比如一些API接口只关注输入与输出而不关注内部的逻辑实现。 5 可测试性设计
5.1 分层设计
将一件复杂的事情进行分解,是提升效率的基本手段,这在日常生活中非常常见。比如汽车的生产过程离散在多个零部件生产线,最后完成组装。软件中的分层设计,也是最常见的一种架构模式,在流行的开发框架中随处可见。分层设计可以帮助单元测试准确的命中目标,比如通常情况下我们并不需要对UI而只希望对核心的业务逻辑进行单元测试,如果没有分层,UI与业务逻辑耦合,就会使单元测试无法准确命中目标甚至寸步难行。 5.2 抽象设计
抽象是增强软件扩展性的一把利剑,主板厂商很早就把抽象应用自如了。比如主板上的USB接口,并不针对某一种具体的设备,而只定义了USB标准:接口尺寸、电流、电压、数据传输协议等,然后依据这个标准生产主板。USB标准即抽象,主板厂商通过抽象获得了对无限种类USB设备的扩展支持。
因为有了USB标准,所以很容易就可以设计生产一个USB测试工装,这个工装就类似于单元测试中的测试替身,主板厂商在测试时,并不需要外接一个用户经常使用的U盘或USB键盘,而只需要外接一个USB测试工装即可完成测试,并且这个测试工装可以在符合USB基本标准的前提下按测试需求设计生产,比如只需要按数据传输标准接收数据即可而并不需要真正的存储数据。 5.3 依赖注入
当发生火警时,消防通道的畅通保障了救援。在单元测试中,依赖注入保障了代码的可测试。由此可见,依赖注入在可测试性编码中的重要性。
如果所有职业按成就感进行排名的话,我想软件开发一定是名列前茅的,因为大多数时候软件开发人员扮演的就是“上帝角色”,他们可以随时new一切需要的对象。但在依赖注入模式下,上帝需要从“自己创造”转变为“习惯组装”。 6 可测试性编码
Google的研发工程师写了一篇关于软件可测试性的文章《Guide: Writing Testable Code》,觉得里面的代码示例比较具有代表性,摘录并整理简化了代码(可不关注语法细节,当作伪代码来看)如下: 6.1 注入协作对象
难以测试的代码示例:
易于测试的代码示例:
6.2 不要依赖静态方法
难以测试的代码示例:
易于测试的代码示例:
6.3 不要依赖全局变量
难以测试的代码示例:
易于测试的代码示例:
6.4 不要为了测试而测试
难以测试的代码示例:
易于测试的代码示例:
7 使用测试框架
7.1 GTest简介
测试框架为我们提供了测试用例管理、断言、参数化、用例执行等系列通用功能,使我们可以专注于测试用例本身业务逻辑的处理。在C/C++编程中,GTest当前最流行的单元测试框架,它由Google公司发布,支持跨平台(Linux、Windows、MacOS),GTest官方仓库地址为:https://github.com/google/googletest 7.2 使用GTest编写单元测试用例
GTest框架会自动执行所有单元测试用例(由TEST、TEST_F等宏定义),一个单元测试用例类似于一个函数,其中第一个参数为测试套件名称,测试套件就是一系列单元测试用例的集合,第二个参数为单元测试用例名称,如上代码所示。
在实际项目中,通常相同类型的多个测试用例需要相同的初始化和清理过程,或需要共用一些资源。此时就可以使用自定义测试套件方式,如上代码所示。 7.3 单元测试覆盖率统计
单元测试覆盖率通常指的是行覆盖率,其计算规则为:分母为被测项目有效代码(排除空白、注释等无效行)的总行数,分子为被单元测试用例执行到的行数,由此计算的比例为单元测试行覆盖率。华为大多软件项目对外宣称的单元测试行覆盖率为70%,根据我的经验,这是一个相当高的比例了。 有很多统计单元测试覆盖率的工具,比如针对C++的 OpenCppCoverage ,安装后通过一条命令即可生成HTML可视化的单元测试覆盖率统计报表:
8 单元测试的成败关键
8.1 时间与成本预算
决定在项目中实施单元测试前,需要与项目经理充分沟通项目时间周期与成本,因为单元测试需要增加开发工程师在编码阶段的时间投入,这个比例大致在0.5~1.0之间。即假如某个功能的编码时间是10天,那么需要增加大约5-10天来完成单元测试。同时单元测试并非一劳永逸,后续当被测试的业务代码发生变更,与之对应的单元测试用例也需要同步变更。因此获得相应的项目资源预算对单元测试的成败至关重要,如果没有给到开发人员相对充裕的时间,但又要求他们达成单元测试指标,就会导致开发人员认为单元测试挤占了功能开发时间,从而排斥单元测试。 8.2 在软件架构设计阶段整体考虑可测试性(架构师)
可测试性架构设计是达成单元测试在技术层面最重要的环节,好比房屋装修,如果软装都完成了,冰箱彩电空调摆放就位,才发现忘了走电源线,那么补救成本就非常高了。 8.3 在编码阶段具备可测试性意识(开发工程师)
除了架构设计提前考虑对单元测试的支持,软件编码亦是如此,开发人员在编写代码前应提前了解单元测试,以不至于编写出来的代码不能或难以进行单元测试。比如全局变量满天飞导致测试用例之间相互影响,类中的协作对象完全不使用依赖注入导致测试用例无从下手,等等。 9 后记
本文介绍了单元测试的基本概念,以及结合实际项目,分享了单元测试实施要点。是对自己项目过程的总结,也希望对有需要的同学有所帮助。 最后做一点补充,实施单元测试大致分为两类,我称之为主动单元测试和被动单元测试,主动单元测试,是以提升代码质量和软件架构为目的,由内部主动发起,实施过程中会同步优化软件架构、提升代码可测试性。而被动单元测试由外部驱使,比如来自客户或市场的外部要求,它以覆盖率为唯一目标,通常会借助一些商业工具(比如Tessy),自动生成单元测试用例与完成打桩,它不需要修改源程序代码,当然也不会提升软件的架构质量。本文所描述的,以及我个人比较推崇的为主动单元测试。 <全文完> 转自https://www.cnblogs.com/wubayue/p/18760269 该文章在 2025/3/10 16:06:07 编辑过 |
关键字查询
相关文章
正在查询... |