前言
这章内容主要是讲单测,单元就是指一个程序分的最小单位,一般是类或者方法,在面向对象编程里,一般就是认为方法是最小单位,单测是程序功能的基本保障,在软件上线前非常重要的一环
正文
单测的好处:
1、提升软件质量:单元测试可以保障开发质量和程序的鲁棒性。开发时不断地测试能帮助发现程序的问题,进而定位和排查。越早发现问题,修复的成本也会越低;三种情况从好到坏:1、提前发现问题 2、出问题快速定位 3、跟在问题后疲于奔命,功能回归
2、 促进代码优化:在测试的过程中,工程师会优化代码的执行效率,时间空间复杂度降低就会让程序更好
3、提升研发效率:编写单测虽然会增加程序开发的时间,但后续排错和维护会大大节约时间,提升整体研发效率
4、增加重构自信:重构是非常困难的,修改一个底层结构就会影响大量上层服务,单测通过就可以提升重构的自信和勇气
单测可以说是测试人员的工作,但对开发人员来说,也是一种基本素养的体现
单元测试的基本原则
单元测试宏观要符合AIR原则,微观符合BCDE原则
新增代码应该同步增加单测用例,修改代码逻辑也要保证单测都能通过
AIR具体原则包括:
A:Automatic自动化:测试用例必须是自动化的,不允许使用System.out验证,必须使用断言验证
I:Independent独立性:独立性就是不允许用例间相互调用,否则导致运行效率降低,或者错误导致程序中断
R:Repetable可重复:主流测试框架中, JUnit 的用例执行顺序是无序的,而TestNG 支持测试用例的顺序执行,单测是可重复执行,不受外部环境影响,有提交代码就会触发单测执行
编写单测要保证粒度足够小,最好到方法级,跨类跨系统的逻辑不负责,这个属于集成测试的范围,需要保证BCDE原则:
B: Border : 边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
C: Correct : 正确的输入, 并得到预期的结果。
D: Design :与设计文档相结合,来编写单元测试。
E : Error :单元测试的目标是证明程序有错,而不是程序无错。为了发现代码中潜在的错误, 我们需要在编写测试用例时有一些强制的错误输入(如非法数据、异常流程、非业务允许输入等)来得到预期的错误结果。
- 由于单测只是运行前的模拟测试,一些因素不具备,可以Mock:
功能因素。比如被测试方法内部调用的功能不可用。
时间因素。比如双十一还没有到来,与此时间相关的功能点。
环境因素。政策环境,如支付宝政策类新功能,多端环境, 如PC 、手机等。
数据因素。线下数据样本过小,难以覆盖各种线上真实场景。
其他因素。为了简化测试编写, 开发者也可以将一些复杂的依赖采用Mock方式实现。
可以使用硬编码实现mock,也可以使用配置文件,也可以使用一些mock框架,例如JMockit 、EasyMock 、JMock 等,使用mock可以隔离一些复杂因素,写出稳定的单测,得到稳定的结果
单元测试覆盖率
单测覆盖是指业务代码被测试的程度,从粗到细,从弱到强如下:
1、粗粒度
通常只要执行到类或方法里的代码,即使只有部分,也是覆盖了这个类或方法,通常这个覆盖率可以达到100%,但也不够
2、细粒度
(1)行覆盖
行就是代码的行,覆盖的行/代码总行数就是行覆盖率
如图有三个入参,5句可执行语句,编写如下测试用例
执行结果
上例中覆盖率100%,但c==3并没有执行到,并且a!=1 并且c!=3也没有执行到,可见覆盖率不代表完整程度,但由于好计算,行测也很常用
(2)分支测试
分支覆盖也叫判定覆盖,就是对各种可能情况进行覆盖判定,与下面的容颜混淆
(3)条件判定覆盖
条件判定覆盖要求所有可能条件都执行一次,并且可能结果也至少执行一次
这里的csvsource就是入参两种,023和103,分别为true和false,覆盖了可能结果
并且第二个000入参算进去,把三个条件的真假都覆盖到了
(4)条件组合覆盖
这个就是把所有条件的排列组合都来一遍,是条件判定覆盖的一个子集
这组有8组入参,就是字面意义上的3个布尔元素排组合,2的三次方,8种,如果是n个条件,就是2的n次方个,可见工作量很大
(5)路径覆盖
路径覆盖要求能够测试到程序中所有可能的路径。在testMethod 方法中,可能的路径有① a= 1, b=2 ② a1b!=2,c3 ③ a=1 ,b!=2 ,c! =3 ④ a! =1,c=3 ⑤ a !=1,c!=3
这5种。当存在“||”时, 如果第一个条件已经为true ,不再计算后边表达式的值。
而当存在“&& ” 时,如果第一个条件已经为false ,同样不再计算后边表达式的值。
这里少了3个,因为去除了判定
单元测试编写
这节介绍如何编写单测
单测框架用的多的有JUnit 和TestNG
这里用Junit5,可能有不兼容Junit4
Junit5有三个模块组成:
JUnit Platform· 用于在NM 上启动测试框架, 统一命令行、Gradle 和Maven、等方式执行测试的人口。
JUnit Jupiter: 包含JUnit5.x 全新的编程模型和扩展机制。
JUnit Vintage : 用于在新的框架中兼容运行JUnit3.x 和JUnit4.x 的测试用例。
Juint有很多注解,如下图
下图是一个测试类的结构
需要注意的是,@DisplayName 注解仅仅对于采用IDE 或图形化方式展示测试运行结果的场景有效
对于使用Maven 或Gradle 等命令行方式运行单元测试的情况,该注解中的内
容会被忽略;例如单元测试出错时,实际展示结果如下·
当测试用例较多时,为了更好地组织测试的结构,推荐使用JUnit 的@Nested 注解来表达有层次关系的测试用例:
Junit没有嵌套限制,但不建议超过三层,否则很难理解
分组测试有一些技巧,比如分为不同强度:“执行很快且很重要”的冒烟测试用例、“执行很慢但同样比较重要”的曰常测试用例,以及“ 数量
很多但不太重要”的回归测试用例。
使用Junit的@Tag注解可以区分强度
通过标签选择执行的用例类型, 在Maven 中可以通过配置maven-surefire-pIugin插件来实现·
这里运行fast而不运行slow
在Gradle 中可以通过JUnit 专用的junitPlatform 配置来实现
一些算法逻辑,计算逻辑很多,需要数据驱动测试,如果按照平常的逻辑,需要写很多用例,使用@TestFactory注解,可以将数据输入输出与测试逻辑分开,一次编写就可对各种结果进行验证
这里的数字意思是传入12小时,20分种,24秒,0毫秒
命名
通常测试类为被测类加Test后缀,如DemoService 的测试类应该命名为DemoServiceTest
如上为目录,单测代码不能再业务目录下,主流Java 测试框架如JUnit, TestNG 测试代码都是默认放在src/test/java 下的,
测试资源文件则放在src/test/resources 下,
规范单测名称有利于提高测试质量
如上,实例二明显更加清楚,可以猜测到时用户令牌出了问题
主流单测有两种:加test前后缀或者should…when…,比如shouldSuccess WhenDecodeUserToken
同时,避免过长名字
断言和假设
定义好测试方法后,再细化就是断言和假设了,断言是封装了一些常用逻辑,不满足则判定失败;假设类似,但它不满足,测试就直接退出,记录状态为跳过
常用的断言被封装在org.junit.jupiter.api.Assertions中,均为静态方法,如下表是一些常用断言方法
假设提供的静态方法更加简单,被封装在org.junit.jupiter.api.Assumptions 类中, 同样为静态方法
断言的选择,要求更精准,比如asse此Equals( 100, result) 语句优于assertTrue(100 == result) 语句。
如果没有合适的断言,也可以自己定义构造条件,然后不符合条件就用fail断言标记为失败
一般断言两个参数,第一个是预期,第二个是实际结果
预期加10,实际减10,运行结果
assertTimeout 和assertTimeoutPreemptively断言区别在于:前者结束后会继续执行记录实际操作时间,后者会直接结束
实际开发中,如果JUnit的断言不能满足需求,可以使用AssertJ
它的特点是流式断言,一个对象可以连接多个判断,进行多次断言
对于自定义JavaBean对象
AssertJ 通过AssertJ assertions generator 来生成对应的XxxAssert 类,然后辅助我们对模板JavaBean 对象进行断言API 判断。AssertJ
assertions generator 有相应的Maven 和Gradle Plugin , 生成这样的代码非常容易, 所以很容易实现对自定义JavaBean 对象的判断需求。它还有一些拓展,DB assertions, Guava assertions等,可以对Mybatis和Hibernate进行单测
如上为Junit的断言,如果用AssertJ就是这样
可见不同判断连接到了一起,流式断言充分利用了Java 8 之后的匿名方法和Stream 类型的特点,很好地对JUnit 断言方法进行了补充。
后记
还有一章是代码规约,这一章其实是另外一本书的缩略,之后有空会把那本书也弄过来