目录
前言
单元测试通常的没有一个良好的通用规范,因此本文在阅读大量文章资料的基础上结合开发中自己的心得体会总结出一版单元测试的规范和指南。
一个完整的研发流程
开发阶段 = 逻辑代码开发 + 编写单测用例
误区:提测时或者上线前才开始写测试用例
一、基本指南
1.单元测试的意义
- 提前发现问题,保证研发质量
- 唯一一次保证代码覆盖率达到100%的测试
- 修正一个软件错误所需的费用将随着软件生命期的进展而上升
- 代码规范、优化,可测试性的代码
- 放心重构
- 自动化执行,多次执行
2.单元测试的对象
- 条件分支
- 依赖调用结果
- 参数正确性
- 并发与幂等
- 可能的异常处理:依赖调用调用异常、基础组件异常、常见未知异常等
- 边界:数组、整形、浮点数边界条件覆盖
3.何处需要单元测试
- 在逻辑复杂的代码中增加单元测试
- 在容易出错的地方增加单元测试
- 不易理解的代码中增加单元测试(明确代码实现的逻辑)
- 在考虑后期需求变更相对较大的代码中增加单元测试(后期需求更变修改代码后可以减少判断已有代码正确性及逻辑性的工作量)
- 外部接口处添加解耦代码,同时增加单元测试
4.单元测试应该测试什么
- Right——结果是否正确?
- B——是否所有的边界条件都是正确的?
- I——是否可以检查反响关联吗?
- C——是否可以用其它手段交叉检查?
- E——是否可以强制错误条件发生?
- P——是否可以满足性能要求?
1.结果是否正确
看程序运行之后的结构和文档是否一致。当然可能很多的时候一个方法没有很完整的文档描述它,那至少也应该有简单的文字描述,否则没有判断是否正确的依据。一个原则是:对于验证被测方法是否正确的这件事情,如果某些做法能够使它变得更加容易,那么就采纳它。
2.边界条件CORRECT
很多bug都会集中在边界附近,所以应该多注意。
- 一致性(Conformance)——值是否符合预期的格式?
- 很多时候,传递给方法的值或者方法运行后产生的值必须符合某种特定的格式。必须考虑的是,如果数据不能像期望的那样符合格式要求,将会出现什么情况。
- 有序性(Ordering)——一组值是该有序的,还是该无序的?
- 应该考虑数据顺序,或者是在一个很大的数据集合中某一数据的位置。
- 区间性(Range)——值是否在一个合理的最大值和最小值的范围之内?
- 对于一个变量,它所属类型的取值范围可能比需要的或想要的更加宽广。比如人的年龄、角度等。
- 引用、耦合性(Reference)——代码是否引用了一些不受代码本身直接控制的外部因素?
- 如果对于类的状态、其它对象的状态,或者全局应用程序的状态,需要作一些假设,那么就需要对代码进行测试,保证其在假设未满足的情况下运行良好。前置条件和后置条件都必须检测。
- 前置条件(preconditions):系统必须处于什么状态下,该方法才能运行。当前置条件不能满足的情况下程序是否能够正确运行。
- 后置条件(postconditions):方法运行之后将会有哪些状态发生。程序的返回结构必须检查,伴随产生的副作用也必须检查。
- 存在性(Existence)——值是否存在(要小心null、0、“”、有时可能还需要注意空格字符串)
- 存在性的问题很容易出现在方法的参数上,也经常出现在方法要用到的类的属性上。特别是引用类型,要特别注意。当遇到null等值时,采取什么策略需要及早考虑。应该形成一贯的处理策略,形成风格。可以考虑抛出异常,并且Message描述问题应尽量细致明确。
- 基数性(Cardinality)——是否恰好有足够的值?
-
- 对基数性问题的认识我目前还不是很透彻。
- 时间性,绝对的或者相当的(Time)——所有事情是否都是按顺序发生的?是否在正确的时间?是否及时?
- 数据格式
- 完全伪造或者不一致的输入数据,格式错误的数据。
- 空值或者不完整的值,如0, 0.0, “ ”, null之类的。
- 一些与意料中的合理值相去甚远的数值。
- 如果是传入一系列数据,要考虑是否允许重复,考虑值是否应该有特定顺序,顺序是否可能有错误等。
3.检查反向关联
对于一些方法,我们可以使用反向的逻辑关系来验证它们。注意当同时编写了原方法和它的反向测试时,一些bug可能会被在两个函数中都出现的错误所掩盖。在可能的情况下,应该使用不同的原理来编写反向测试。
4.使用其他手段来实现交叉检查
通常解决一个问题会有多种手段,如果你选择了其中一个方法,那么就可以用其它的方法来检验它。另一种方法就是:使用类本身不同组成部分的数据,并且确信它们能“合起来”。
5.强制产生错误条件
应当能够通过强制引发错误,来测试代码是如何处理所有这些真实问题的。
下面是一些常见环境方面的因素:
- 内存耗光
- 磁盘用满
- 时钟出问题
- 网络不可用或者有问题
- 系统过载
6.性能特性
一个检查起来会很有益处的部分是性能特性,而不是性能本身。
5.单元测试的步骤
代码编写完成后的单元测试工作主要分为两个步骤:
5.1 人工静态检查
人工静态检查是测试的第一步,这个阶段工作主要是保证代码算法的逻辑正确性(尽量通过人工检查发现代码的逻辑错误)、清晰性、规范性、一致性、算法高效性。并尽可能的发现程序中没有发现的错误。
通常在人工检查阶段必须执行以下项目的活动:
- 检查算法的逻辑正确性。确定所编写的代码算法、数据结构定义(如:队列、堆栈等)是否实现了模块或方法所要求的功能。
- 模块接口的正确性检查。确定形式参数个数、数据类型、顺序是否正确;确定返回值类型及返回值的正确性。
- 输入参数有没有作正确性检查。如果没有作正确性检查,确定该参数是否的确无需做参数正确性检查,否则请添加上参数的正确性检查。经验表明,缺少参数正确性检查的代码是造成软件系统不稳定的主要原因之一。
- 调用其他方法接口的正确性。检查实参类型正确与否、传入的参数值正确与否、个数正确与否,特别是具有多态的方法。返回值正确与否,有没有误解返回值所表示 的意思。最好对每个被调用的方法的返回值用显湿代码作正确性检查,如果被调用方法出现异常或错误程序应该给予反馈,并添加适当的出错处理代码。
- 出错处理。模块代码要求能预见出错的条件,并设置适当的出错处理,以便在一旦程序出错时,能对出错程序重做安排,保证其逻辑的正确性,这种出错处理应当是 模块功能的一部分。若出现下列情况之一,则表明模块的错误处理功能包含有错误或缺陷:出错的描述难以理解;出错的描述不足以对错误定位,不足以确定出错的 原因;显示的错误信息与实际的错误原因不符;对错误条件的处理不正确;在对错误进行处理之前,错误条件已经引起系统的干预等。
- 保证表达式、SQL语句的正确性。检查所编写的SQL语句的语法、逻辑的正确性。对表达式应该保证不含二义性,对于容易产生歧义的表达式或运算符优先级 (如:《 、=、 》、 &&、||、++、 --等)可以采用扩号“()”运算符避免二义性,这样一方面能够保证代码的正确可靠,同时也能够提高代码的可读性。
- 检查常量或全局变量使用的正确性。确定所使用的常量或全局变量的取值和数值、数据类型;保证常量每次引用同它的取值、数值和类型的一致性。
- 表示符定义的规范一致性。保证变量命名能够见名知意,并且简洁但不宜过长或过短、规范、容易记忆、最好能够拼读。并尽量保证用相同的表示符代表相同功能,不要将不同的功能用相同的表示符表示;更不要用相同的表示符代表不同的功能意义。
- 程序风格的一致性、规范性。代码必须能保证符合企业规范,保证所有成员的代码风格一致、规范、工整。例如对数组做循环,不要一会儿采用下标变量从下到上的 方式(如:for(I=0;I++;I<10)),一会儿又采用从上到下的方式 (如:for(I=10;I--;I>0));应该尽量采用统 一的方式,或则统一从下到上,或则统一从上到下。建议采用for循环和While循环,不要采用do{}while循环等。
- 检查程序中使用到的神秘数字是否采用了表示符定义。神秘的数字包括各种常数、数组的大小、字符位置、变换因子以及程序中出现的其他以文字形式写出的数值。 在程序源代码里,一个具有原本形式的数对其本身的重要性或作用没提供任何指示性信息,它们也导致程序难以理解和修改。对于这类神秘数字必须采用相应的标量 来表示;如果该数字在整个系统中都可能使用到务必将它定义为全局常量;如果该神秘数字在一个类中使用可将其定义为类的属性(Attribute),如果该 神秘数字只在一个方法中出现务必将其定义为局部变量或常量。
- 检查代码是否可以优化、算法效率是否最高。如:SQL语句是否可以优化,是否可以用1条SQL语句代替程序中的多条SQL语句的功能,循环是否必要,循环中的语句是否可以抽出到循环之外等。
- 检查程序是否清晰简洁容易理解。注意:冗长的程序并不一定不是清晰的。
- 检查方法内部注释是否完整。是否清晰简洁;是否正确的反映了代码的功能,错误的注释比没有注释更糟;是否做了多余的注释;对于简单的一看就懂的代码没有必要注释。
- 检查注释文档是否完整。对包、类、属性、方法功能、参数、返回值的注释是否正确且容易理解;是否会落了或多了某个参数的注释,参数类型是否正确,参数的限 定值是否正确。特别是对于形式参数与返回值中关于神秘数值的注释,如:类型参数 应该指出 1.代表什么,2.代表什么,3.代表什么等。对于返回结果集(Result Set)的注释,应该注释结果集中包含那些字段及字段类型、字段顺序等。
5.2 动态执行跟踪
执行待测程序来跟踪比较实际结果与预期结果来发现错误。经验表明,使用人工静态检查法能够有效的发现30%到70%的逻辑设计和 编码错误。但是代码中仍会有大量的隐性错误无法通过视觉检查发现,必须通过跟踪调试法细心分析才能够捕捉到。所以,动态跟踪调试方法也成了单元测试的重点与难点
6.如何写出高质量的单元测试
6.1. 编写好的测试用例
- case名称清晰明确
- case设计中要考虑边界
- 好的单元测试完备⽽不重复
- 设计case,是基于意图的设计,而不是基于实现
- 善用setup,将通用的初始化进行整理
- 要明确测试意图,尤其对最可能出错、最有风险、逻辑最重、计算的地方进行用例覆盖
- 把被测函数分为几部分逻辑,针对每一块设计case
- 需要mock的,是调用外部资源、请求、数据的,或者较难实现的错误场景等。
- 对bug要分析,要写单测覆盖它
- 不会出错的,set/get,一句if的,内联的,逻辑很简单的,不用写单测
6.2. 如何设计单元测试用例
- 正常逻辑,对于输入正确的参数,会有符合期望的输出结果。
- 异常&边界逻辑处理,对于错误的参数,或者边界点的参数时,不要抛出不可预知的异常。
- 单元测试可以覆盖该方法的所有分支,循环,尽可能达到100%的行覆盖率&逻辑覆盖率。
- 对于所有的条件分支以及嵌套的方法,尽可能多的去触发条件,并保证内部调用的方法也做到覆盖。
6.3. 单元测试的代码结构
⼀般一个三步经典结构:准备,调⽤,断⾔。
- 准备部分的⽬的是准备好调⽤所需要的外部环境,如数据,Stub,Mock,临时变量,调⽤请求,环境背景变量等等。
- 调⽤部分则是实际调⽤需要测试⽅法,函数或者流程。
- 断⾔部分判断调⽤部分的返回结果是否符合预期。
6.4. 单元测试mock
在做单元测试的时候,我们会发现我们要测试的方法会引用很多外部依赖的对象,如调用平台接口、连接数据库、网络通讯、远程服务、FTP、文件系统等等。 而我们没法控制这些外部依赖的对象,为了解决这个问题,我们就需要用到Mock工具来模拟这些外部依赖的对象,来完成单元测试。
现在比较流行的Mock工具有JMock、EasyMock、Mockito、PowerMock。
- 动态代理:Mockito、EasyMock、MockRunner
- 自定义类加载器:PowerMock
- 运行时字节码修改:JMockit、TestableMock
推荐使用的是Mockito和PowerMock。PowerMock弥补了其他3个Mock工具不能mock静态、final 、私有方法的缺点。
当用例出现下面的情况我们可以使用Mock对象来完成单元测试:
- 真实对象具有不可确定的行为,会产生不可预测的结果。 如:数据库查询可以查出一条记录、多条记录、或者返回数据库异常等结果。
- 真实对象很难被创建。如:平台代码,或者Web、JBoss容器等。
- 真实对象的某些行为很难触发。 如需要对此缓存队列进行Mock,根据调用返回不同的数据量给测试。
- 测试需要知道真实对象是如何被调用的。如:测试用例需要验证是否发送了JMS,此时就可以通过Mock对象是否被调用来测试。
- 真实对象实际不存在时。 如:当我们与其他模块交互时,或者与新的接口打交道时,更有就是对方的代码还没有开发完毕时,我们可以通过Mock来模拟接口的行为,实现代码逻辑的验证和测试。
具体Mockito和PowerMock用法可以参考 工作多年后我更体会到单元测试的重要性
二、具体规范
1.结构及命名规范
1.1单测包结构
1、test包结构包名必须同main一致
2、对应类的单元测试类命名:${className}Test,
eg: main:com.hellobike.ride.application.RideIfaceImpl ===> test:com.hellobike.ride.application.RideIfaceImplTest
如下图:
1.2单测方法命名
单元测试方法命名:test${MethodName}${Scene},以驼峰形式命名
eg:reserve ===> testReserveSuccess、testReserveOverTimes、testReserveInvalidParams
如下图:
2.规范细则
1. 【强制】好的单元测试必须遵守 AIR 原则。
说明: 单元测试在线上运行时,感觉像空气(AIR)一样并不存在,但在测试质量的保障上, 却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。
-
- A: Automatic(自动化) :判断正确与否不需要人工干预,必须通过Assert来判断单测正确性
- I: Independent(独立性):单元测试用例之间决不能互相调用,也不能依赖执行的先后次序,保证测试用例好维护
- R: Repeatable(可重复):单元测试是可以重复执行的,不能受到外界环境的影响。需要把相关外部依赖都mock掉
2. 【强制】单元测试应该是全自动执行的,并且非交互式的。测试框架通常是定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证。
3. 【强制】保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间绝对不能互相调用,也不能依赖执行的先后次序。
反例: method2 需要依赖 method1 的执行,将执行结果做为 method2 的输入。
4. 【强制】单元测试是可以重复执行的,不能受到外界环境的影响。
说明: 单元测试通常会被放到持续集成中,每次有代码check in时单元测试都会被执行。如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。
正例: 为了不受外界环境影响,要求设计代码时就把 SUT 的依赖改成注入,在测试时用 spring这样的 DI 框架注入一个本地(内存)实现或者 Mock 实现。
5. 【强制】对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别。
说明: 只有测试粒度小才能在出错时尽快定位到出错位置。单测不负责检查跨类或者跨系统的交互逻辑,那是集成测试的领域。
6. 【强制】核心业务、核心应用、核心模块的增量代码确保单元测试通过。
说明: 新增代码及时补充单元测试,如果新增代码影响了原有单元测试,请及时修正。
7. 【强制】单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下。
说明: 源码构建时会跳过此目录,而单元测试框架默认是扫描此目录。
8. 【推荐】单元测试的基本目标:语句覆盖率达到 70%;核心模块的语句覆盖率和分支覆盖率都要达到 100%
说明: 在工程规约的应用分层中提到的 DAO 层,Manager 层,可重用度高的 Service,都应该进行单元测试。
9. 【推荐】编写单元测试代码遵守 BCDE 原则,以保证被测试模块的交付质量。
-
- B: Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
- C: Correct,正确的输入,并得到预期的结果。
- D: Design,与设计文档相结合,来编写单元测试。
- E: Error,强制错误信息输入(如:非法数据、异常流程、非业务允许输入等),并得 到预期的结果。
10. 【推荐】对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的, 或者直接操作数据库把数据插入进去,请使用程序插入或者导入数据的方式来准备数据。
反例: 删除某一行数据的单元测试,在数据库中,先直接手动增加一行作为删除目标,但是这 一行新增数据并不符合业务插入规则,导致测试结果异常。
11. 【推荐】和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。或者 对单元测试产生的数据有明确的前后缀标识。
正例:在 RDC 内部单元测试中,使用 RDC_UNIT_TEST_的前缀标识数据。
12. 【推荐】对于不可测的代码建议做必要的重构,使代码变得可测,避免为了达到测试要求而 书写不规范测试代码。
13. 【推荐】在设计评审阶段,开发人员需要和测试人员一起确定单元测试范围,单元测试最好 覆盖所有测试用例(UC)。
14. 【推荐】单元测试作为一种质量保障手段,不建议项目发布后补充单元测试用例,建议在项 目提测前完成单元测试。
15. 【参考】为了更方便地进行单元测试,业务代码应避免以下情况:
-
- 构造方法中做的事情过多。
- 存在过多的全局变量和静态方法。
- 存在过多的外部依赖。
- 存在过多的条件语句。
说明: 多层条件语句建议使用卫语句、策略模式、状态模式等方式重构。
16. 【参考】不要对单元测试存在如下误解:
-
- 单元测试代码是多余的。汽车的整体功能与各单元部件的测试正常与否是强相关的。
- 单元测试代码不需要维护。一年半载后,那么单元测试几乎处于废弃状态。
- 单元测试与线上故障没有辩证关系。好的单元测试能够最大限度地规避线上故障。
3.规范检查与奖惩
单元测试覆盖目标增量覆盖70%,全量覆盖50%,若覆盖度未达到,atlas研发平台将拒绝相关分支代码发布。
三、常用工具
1.Squaretest
1.1 介绍
Squaretest,是一款IDEA插件,基于Velocity模板生成单测,会帮每个对应的被测方法生成一个测试类,spring的注入类会被mock掉,没有参数推导,所以参数还需要用户自行填充。
1.2 使用方法
1、IDEA安装插件“Squaretest”,如下图:
2、打开需要被单测覆盖的类,然后右键,点击“Generate…”(中文为生成)
3、点击“Generate Test - Confirm Mocks”,如下图:
4、然后选中需要mock的属性,点击ok。即自动在test包下面生成单元测试。
5、需要被单测的类中的自动注入全部由Mock产生。内容如下图:
原始类:
自动生成单测类:
6.运行结果:
原始类方法中会有filter方法,因此单测时可以分为是否有过滤两种情况。
有过滤情况:
运行结果:
无过滤情况:
运行结果:
方法覆盖率:
2.DiffBlue
2.1 介绍
Diffblue 是一款基于AI来编写单测的工具,它分析现有的Java应用程序,并编写反映当前行为的单元测试,从而增加测试范围并帮助您在将来的代码更改中查找回归。Cover在代码更改时通过更新测试来自动维护测试。Cover支持标准Java 8和11,Spring和Spring Boot,但是需要springboot 2.x起。
2.2 使用方法
1.IDEA安装插件“DiffBlue”,如下图:
2.点击左侧图标或右键生成
3.自动生成测试类
注意:不是很稳定,可能出现错误,需要程序员自己修改