51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

新领导来了3天,我开始加班写单元测试!然后怒肝了- SpringBoot单元测试指南

" 最近公司来了新领导,所谓新官上任三把火。领导review了一遍公司的代码,发现大部分代码的测试覆盖率极低。之后每个部门都动员起来,填补原来单元测试的债".小B对我吐糟道。 指北君见状立马连夜肝了这一篇,SpringBoot单元测试指南。

1 单元测试的优点与基本原则 {#1-单元测试的优点与基本原则}

单元测试,是指对程序中的最小可测试单元进行验证,在Java中的话,就是类。其有两个目的

  1. 验证程序实现的逻辑是否与设计的逻辑正确
  2. 在涉及到代码修改时,用单元测试去保证原有功能不被破坏,

而一个好的单元测试应该具备以下FIRST 原则和AIR原则中的任何一条:

单元测试的FIRST 规则

Fast 快速原则,测试的速度要比较快,

Independent 独立原则,每个测试用例应该互不影响,不依赖于外部资源。

Repeatable 可重复原则,同一个测试用例多次运行的结果应该是相同的

Self-validating 自我验证原则,单元测试可以自动验证,并不需要手工干预

Thorough 及时原则 单元测试必须即使进行编写,更新,维护。保证测试用例随着业务动态变化

AIR原则

Automatic 自动化原则 单元测试应该是自动运行,自动校验,自动给出结果。

Independent 独立原则 单元测试应该独立运行,吧相互之间无依赖,对外无依赖,多次运行之间无依赖。

Repeatable 可重复原则 单元测试是可重复运动的,每次的结果都稳定可靠。

一个整套完善的单元测试可以保障后续的增添功能时,程序迭代过程中,代码的逻辑正确性。验证程序的输入和输出与最初设计一致。这对后续的集成测试等会提供巨大的帮助。同时也会有利于集成测试的顺利进行。

2 单元测试的粒度应该如何选择 {#2-单元测试的粒度应该如何选择}

一个好的单元测试基本上应该满足上面讲到的FIRST 规则和AIR原则,然而关于单元测试需要覆盖到那些层级,覆盖到那些点,这个直接和写单元测试所花费的时间相关。所以一个团队应该要考虑单元测试的粒度,以及单元测试的代码覆盖率。

有些厂追求100%的单元测试覆盖率,本人觉得完全没有必要。stack overflow上也有一些讨论,有些点赞比较高的回答,大概意思就是:老板付钱是让我们写代码,而不是做测试。测试目的是保障代码逻辑符合预期的结果。然而有一些基本代码,没有必要专门去写单元测试中。而写单元测试应该要针对一些有意义的错误做测试,对比较复杂的逻辑需要囊括的单元测试也会比较多。把单元测试的90%的精力放到那些复杂且有意义的代码片段上。

目前一些代码扫描工具基本都给出了最低30%的单元测试代码覆盖率。这是一个最低限度,然而一个项目的覆盖率,要综合去考虑项目成本,人员安排等等因素。

关于单元测试的粒度,可以总结以下几点:

  1. DAO层的单元测试: 对于基本CRUD,可以考虑跳过这一部分单元测试,而一些比较复杂的动态更新、查询等操作,建议用使用H2去做模拟单元测试。
  2. Service层的单元测试:基本上一个Service里面肯定会依赖很多其他的service(此处也建议将成员变量通过构造方法进行注入,以便于单元测试去Mock),此时建议我们将依赖其他service的方法用Mock替代,Service里面的一些数据库的操作也进行Mock。这样可以保证service测试的独立性,不过对于逻辑复杂的方法可能要花很多时间在Mock上面。 如果发现需要Mock的方法过多,那么可能就需要考虑将要测试的方法是不是需要重构。
  3. Controller(API)层的单元测试:主要着重测试HTTP status在 200,400,500 等情况下的异常处理,request及response的转换等。由于其余部分的代码测试都已经在其对应的单元测试覆盖,那么此时可以Mock绝大部分Serivce层中的方法。
  4. 一般工具类的单元测试:一些工具类里面包含了比较多的逻辑,所以需要尽可能考虑多种情况下测试用例。

3 单元测试简单示例 {#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 2 3 4 5 6 7 8 9 10 11 12 13 | class UserServiceTest { @BeforeEach void setUp() { } @AfterEach void tearDown() { } @Test void saveUser() { } @Test void retrieveUserById() { } } |

我们加上一些测试必要的注解(此时需要引入junit依赖),并写简单的单元测试。

|---------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @RunWith(MockitoJUnitRunner.class) @ExtendWith({MockitoExtension.class}) class UserServiceTest { UserService userService; @Mock UserRepository userRepository; @BeforeEach void setUp() { userService = new UserServiceImpl(userRepository); } @AfterEach void tearDown() { } @Test void saveUser() { } @Test void retrieveUserById() { User user1 = new User("javaNorth",100l,"指北君"); when(userRepository.findById(anyLong())).thenReturn(Optional.of(user1)); User user = userService.retrieveUserById(101L); assertThat(user1).isEqualTo(user); } } |

上面的示例我们Mock了userRepository.findById()这个方法在UserServiceImp.retrieveUserById 中的调用。这就可以测试到retrieveUserById方法中其余的代码逻辑,从而与userRepository隔离开来。

4 SpringBoot中的单元测试 {#4-springboot中的单元测试}

SpringBoot中的单元测试可以启动服务,可以不启动服务。而在非必要的情况尽量避免启动整个SpringBoot服务。如果想要测试跑的更快,那么就要尽量少的实例化不需要的类。

下面我们看一下写单元测试时常用的注解。

@SpringBootTest {#springboottest}

SpringBoot的单元测试可以使用@SpringBootTest 注解。其中可以在此注解参数中指定需要加载的类,这样有助于Spring快速启动并完成测试。

|---------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 | @RunWith(SpringRunner.class) @SpringBootTest(classes = {MapRepository.class, CarService.class}) public class CarServiceWithRepoTest { @Autowired private CarService carService; @Test public void shouldReturnValidDateInTheFuture() { Date date = carService.schedulePickup(new Date(), new Route()); assertTrue(date.getTime() > new Date().getTime()); } } |

@DataJpaTest {#datajpatest}

对于JPA,Repository进行测试的时候可以使用@DataJpaTest 注解,有了这个注解,Spring在启动的时候就只会加载@Repository 相关的class。同样可以提高测试的效率。

|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @RunWith(SpringRunner.class) @DataJpaTest class UserRepositoryTest { @Autowired UserRepository userRepository; @Test public void findByUserIdReturnUser() { User user1 = new User("javaNorth",100l,"指北君"); Optional<User> userOptional = userRepository.findById(100l); assertThat(user1).isEqualTo(userOptional.get()); } } |

@WebMvcTest web层的测试 {#webmvctest-web层的测试}

测试WEB层可以使用@WebMvcTest 注解,使用此注解可以测试controller部分并且不用把整个服务都跑起来。也是一种提高测试速度的方法

|------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | @RunWith(SpringRunner.class) @WebMvcTest(UserController.class) class UserControllerTest { @Autowired private MockMvc mvc; @MockBean private UserService userService; @InjectMocks UserController userController; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); mvc = MockMvcBuilders.standaloneSetup(userController).build(); } @AfterEach void tearDown() { } @Test void retrieveUserByUserId() throws Exception { User user1 = new User("javaNorth","100","指北君"); given(this.userService.retrieveUserById("100")) .willReturn(user1); this.mvc.perform(MockMvcRequestBuilders.get("/javanorth/user/100") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andDo(MockMvcResultHandlers.print()); } } |

5 单元测试的其他 {#5-单元测试的其他}

@Test注解可以添加返回期望的异常,异常处理的测试也是需要考虑的地方。

|-----------|---------------------------------------------------| | 1 | @Test(expected = IndexOutOfBoundsException.class) |

Karate + Cucumber 可以做基于WEB-API的自动化测试框架,也可以作为Controller部分的测试补充。

另外PowerMock也是一个比较好用的单元测试三方库,功能强大,可以对静态方法,构造方法,私有方法及Final方法进行模拟。后面指北君也会对PowerMock进行单独的介绍,尽情期待哦。

PowerMock

总结 {#总结}

本篇介绍了单元测试的一些概念以及依赖关系,并且给出了不同测试点的代码测试案例。实际项目中需要Mock的方法会比较多,这也就需要花费比较大的精力,虽然使用直接启动SpringBoot的方式可以避免许多Mock方法,但是这样做的话测试速度会明显降低,而且测试用例的独立性也会收到影响。至此指北君还是推荐大家多Mock相关依赖类的方法,以保证单元测试的独立性。PowerMock也是一个比较好用的单元测试

都看到这里了,还不动手撸一套单元测试。如果还有什么想法,不妨在评论区留言。

赞(2)
未经允许不得转载:工具盒子 » 新领导来了3天,我开始加班写单元测试!然后怒肝了- SpringBoot单元测试指南