环境:Spring Boot 2.5.6 + H2 + Lombok + Junit4 + Mockito
一.提高单元测试覆盖率的意义与价值
在想如何提单元覆盖率之前,我们需要了解什么是单元测试覆盖率,单元测试覆盖率是一种软件测试的度量指标,指在所有功能代码中,完成了单元测试的代码所占的比例。
单元测试覆盖率 = 被测代码行数 / 参测代码总行数 * 100% (行覆盖率 / 语句覆盖)
然后,我们做单元测试是为了什么呢。
1. 是想通过单元测试来保证代码质量?
仔细一想就会发现,单元测试高它并不能保证代码的质量,也就是说两者没有什么联系。单元测试覆盖率完全是可以“造假”,例如在单元测试的时候写一些没有实际业务价值的测试用例,使覆盖率**“虚高”**。
2. 是想通过单元测试保证业务逻辑不会出错?
一个业务功能的实现并不仅仅依赖于某一个方法、某一个类,那么通过单元测试能够保证的业务逻辑也是十分有限的,不可能做到**“不会出错”**。不然怎么还有集成测试,组件测试等。
3.那么既然不能保证质量又不能保证业务逻辑不会出错,我们提高覆盖率还有什么意义与价值呢?
1.如果底层测试不够充分就只能靠顶层测试来保证,而越往顶层所需要花费的成本也就越高。
2.在你写的测试代码是值得信任的前提下,测试覆盖率可以快速帮我们找到没有测试的代码,让我们在重构、优化这些没有被测试的代码时更有底气。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gTMejNK7-1680883261788)(https://qijian-1301807797.cos.ap-guangzhou.myqcloud.com/markdown/%E6%B5%8B%E8%AF%95%E9%87%91%E5%AD%97%E5%A1%94%E7%A4%BA%E6%84%8F%E5%9B%BE.png)]
note:想要了解更多:
单元测试:最下层是单元测试,单元测试是测试稳固的根基,因此也是金字塔结构的最底层。
说了这么多,单元测试覆盖率的意义,接下来就来搞清楚单元测试。
二.单元测试进行中
1. 什么是单元测试
定义:单元测试是开发者编写的一小段代码,用于检验代码的一个很小的,很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件下特定函数的行为(《单元测试之道 Java版》)。换句话说,指对软件中的最小可测试单元进行检查和验证,针对的是类和方法。
2. 为什么要做单元测试
执行单元测试,是为了证明某段代码的的行为确实和开发者所期待的一直。
这个简单有效的技术就也为了令代码更加完美。当基层的代码不再可靠时,那么必须要的改动就无法只局限在底层(底层代码修改高层代码也需要需改)。于是,一个对底层代码的修改会导致几乎所有一连串的代码需要修改,修改就会越来越多,越来越复杂。由于地层代码的不可以甚至有可能导致项目的整体以失败而告终。
3. 单元测试中的FIRST原则
3.1 F(Fast):快速
在调试bug时,需要频繁去运行单元测试验证结果是否正确。如果单元测试足够快速,就可以省去不必要浪费的时间,提高工作效率。
@SpringBootTest会启动整个项目,连接数据库和redis,就出现很慢的问题。
3.2 I(Isolated):隔离
好的单元测试是每个单元测试只关注逻辑的一个方面,每个单元测试之间不应该产生依赖,不会因为测试顺序的不同导致运行结果的不同。不要依赖和修改外部数据等其他共享的资源,做到测试前后的一致性。
使用junit编写单元测试时,不同层级之间存在依赖关系。例如:service依赖dao,dao依赖数据库。
3.3 R(Repeatable):可重复
单元测试需要保持运行稳定,每次运行都需要得到同样的结果,如果间歇性的失败,会导致我们不断的去查看这个测试,不可靠的测试也就失去了意义。
使用junit同样不能做到可重复,因为依赖数据库数据的原因,当数据变动后。单元测试的结果也就可能会不一样。
3.4 S(Self-verifying):自我验证
单元测试需要采用Assert函数等进行自验证,即当单元测试执行完毕之后就可得知测试结果,全程无需人工接入。
使用junit单元测试,不能做到自我验证。
3.5 T(Timely):及时
等代码稳定运行再来补齐单元测试可能是低 效的,最有效的方式是在写好功能函数接口后(实现函数功能前)进行单元测试。
4. Java流行的mock框架选择
注意:mock工具还有EasyMock 、WireMock、JMockit、Mock、Moco
Mockito是Java流行的一种Mock框架,使用Mock技术能让我们隔离外部依赖以便对我们自己的业务逻辑代码进行单元测试,在编写单元测试时,不需要再进行繁琐的初始化工作, 在需要调用某一个接口时,直接模拟一个假方法,并任意指定方法的返回值。 Mockito的工作原理是通过创建依赖对象的proxy,所有的调用先经过proxy对象,proxy对象拦截了所有的请求再根据预设的返回值进行处理。
三.Mockito单元测试的正确姿势
1.前置概念
1.1 被测对象:
即我们想要测试的对象,比如userService、xxUtils等。
1.2 Mock 对象
一般为我们被测对象的依赖对象。典型如被测对象的成员变量。主要是一些测试中我们不关注的对象。我们只想要得到这些对象的方法的返回值。而不关注这些方法的具体执行逻辑。此时我们可以将这些对象创建为mock对象。
1.3 Stub(桩)
桩指的是用来替换具体功能的程序段。桩程序可以用来模拟已有程序的行为或是对未完成开发程序的一种临时替代。也就是对调用方法的模拟。
1.4 spy对象:
和mock对象一样,它可以作为被测对象的依赖对象。此时它和mock对象的最大的区别是mock对象的方法如果没有被存根,调用时会返回相应对象的空值;而spy对象的方法被调用时则会调用真实的代码逻辑。
spy方法会走真实的方法,而mock对象不会,spy()方法的参数是对象的实例,mock的参数是class
2. 为什么要mock
在写单元测试的过程中,我们往往会遇到要测试的类有很多依赖,这些依赖的类/对象/资源又有别的依赖,从而形成一 个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。