” 最近公司来了新领导,所谓新官上任三把火。领导review了一遍公司的代码,发现大部分代码的测试覆盖率极低。之后每个部门都动员起来,填补原来单元测试的债“.小B对我吐糟道。 指北君见状立马连夜肝了这一篇,SpringBoot单元测试指南。
1 单元测试的优点与基本原则
单元测试,是指对程序中的最小可测试单元进行验证,在Java中的话,就是类。其有两个目的
- 验证程序实现的逻辑是否与设计的逻辑正确
- 在涉及到代码修改时,用单元测试去保证原有功能不被破坏,
而一个好的单元测试应该具备以下FIRST 原则和AIR原则中的任何一条:
单元测试的FIRST 规则
Fast 快速原则,测试的速度要比较快,
Independent 独立原则,每个测试用例应该互不影响,不依赖于外部资源。
Repeatable 可重复原则,同一个测试用例多次运行的结果应该是相同的
Self-validating 自我验证原则,单元测试可以自动验证,并不需要手工干预
Thorough 及时原则 单元测试必须即使进行编写,更新,维护。保证测试用例随着业务动态变化
AIR原则
Automatic 自动化原则 单元测试应该是自动运行,自动校验,自动给出结果。
Independent 独立原则 单元测试应该独立运行,吧相互之间无依赖,对外无依赖,多次运行之间无依赖。
Repeatable 可重复原则 单元测试是可重复运动的,每次的结果都稳定可靠。
一个整套完善的单元测试可以保障后续的增添功能时,程序迭代过程中,代码的逻辑正确性。验证程序的输入和输出与最初设计一致。这对后续的集成测试等会提供巨大的帮助。同时也会有利于集成测试的顺利进行。
2 单元测试的粒度应该如何选择
一个好的单元测试基本上应该满足上面讲到的FIRST 规则和AIR原则,然而关于单元测试需要覆盖到那些层级,覆盖到那些点,这个直接和写单元测试所花费的时间相关。所以一个团队应该要考虑单元测试的粒度,以及单元测试的代码覆盖率。
有些厂追求100%的单元测试覆盖率,本人觉得完全没有必要。stack overflow上也有一些讨论,有些点赞比较高的回答,大概意思就是:老板付钱是让我们写代码,而不是做测试。测试目的是保障代码逻辑符合预期的结果。然而有一些基本代码,没有必要专门去写单元测试中。而写单元测试应该要针对一些有意义的错误做测试,对比较复杂的逻辑需要囊括的单元测试也会比较多。把单元测试的90%的精力放到那些复杂且有意义的代码片段上。
目前一些代码扫描工具基本都给出了最低30%的单元测试代码覆盖率。这是一个最低限度,然而一个项目的覆盖率,要综合去考虑项目成本,人员安排等等因素。
关于单元测试的粒度,可以总结以下几点:
- DAO层的单元测试: 对于基本CRUD,可以考虑跳过这一部分单元测试,而一些比较复杂的动态更新、查询等操作,建议用使用H2去做模拟单元测试。
- Service层的单元测试:基本上一个Service里面肯定会依赖很多其他的service(此处也建议将成员变量通过构造方法进行注入,以便于单元测试去Mock),此时建议我们将依赖其他service的方法用Mock替代,Service里面的一些数据库的操作也进行Mock。这样可以保证service测试的独立性,不过对于逻辑复杂的方法可能要花很多时间在Mock上面。 如果发现需要Mock的方法过多,那么可能就需要考虑将要测试的方法是不是需要重构。
- Controller(API)层的单元测试:主要着重测试HTTP status在 200,400,500 等情况下的异常处理,request及response的转换等。由于其余部分的代码测试都已经在其对应的单元测试覆盖,那么此时可以Mock绝大部分Serivce层中的方法。
- 一般工具类的单元测试:一些工具类里面包含了比较多的逻辑,所以需要尽可能考虑多种情况下测试用例。
3 单元测试简单示例
单元测试可使用的第三方工具非常多,然而我们不可能精通每一个,只有在不断 的使用提高熟练程度。 对于SpringBoot而言可以直接引入spring-boot-starter-test , 它将会引入JUnit、Spring Test、AssertJ、Hamcrest(匹配对象)、Mockito、JSONassert、JsonPath等工具库。
IDEA编译器可以快速生成单元测试类,其步骤如下:打开当前类 -> Navigate -> Test -> Create New Test .一般情况下建议勾选自动创建Before/After 的方法。IDEA自动创建的Test类如下所示:
1 |
|
我们加上一些测试必要的注解(此时需要引入junit依赖),并写简单的单元测试。
1 |
|
上面的示例我们Mock了userRepository.findById()这个方法在UserServiceImp.retrieveUserById 中的调用。这就可以测试到retrieveUserById方法中其余的代码逻辑,从而与userRepository隔离开来。
4 SpringBoot中的单元测试
SpringBoot中的单元测试可以启动服务,可以不启动服务。而在非必要的情况尽量避免启动整个SpringBoot服务。如果想要测试跑的更快,那么就要尽量少的实例化不需要的类。
下面我们看一下写单元测试时常用的注解。
@SpringBootTest
SpringBoot的单元测试可以使用@SpringBootTest 注解。其中可以在此注解参数中指定需要加载的类,这样有助于Spring快速启动并完成测试。
1 |
|
@DataJpaTest
对于JPA,Repository进行测试的时候可以使用@DataJpaTest 注解,有了这个注解,Spring在启动的时候就只会加载@Repository 相关的class。同样可以提高测试的效率。
1 |
|
@WebMvcTest web层的测试
测试WEB层可以使用@WebMvcTest 注解,使用此注解可以测试controller部分并且不用把整个服务都跑起来。也是一种提高测试速度的方法
1 |
|
5 单元测试的其他
@Test注解可以添加返回期望的异常,异常处理的测试也是需要考虑的地方。
1 |
|
Karate + Cucumber 可以做基于WEB-API的自动化测试框架,也可以作为Controller部分的测试补充。
另外PowerMock也是一个比较好用的单元测试三方库,功能强大,可以对静态方法,构造方法,私有方法及Final方法进行模拟。后面指北君也会对PowerMock进行单独的介绍,尽情期待哦。
总结
本篇介绍了单元测试的一些概念以及依赖关系,并且给出了不同测试点的代码测试案例。实际项目中需要Mock的方法会比较多,这也就需要花费比较大的精力,虽然使用直接启动SpringBoot的方式可以避免许多Mock方法,但是这样做的话测试速度会明显降低,而且测试用例的独立性也会收到影响。至此指北君还是推荐大家多Mock相关依赖类的方法,以保证单元测试的独立性。PowerMock也是一个比较好用的单元测试
都看到这里了,还不动手撸一套单元测试。如果还有什么想法,不妨在评论区留言。