Springboot+Junit5微服务单元测试编写实践

4,875 阅读2分钟

现在写单元测试的重要性不言而喻,下边说明一下Junit5测试的会用到的主要注解和方法。PS:常用开发工具都可以自动生成Junit测试类。

一. 单元测试命名

可参考【翻译】7种流行的单元测试命名约定
在项目种我选择的是should...when的写法,但需注意方法命名时不要过长,过长反而导致难于阅读和理解

二. 常用注解

  • 类注解

    • @SpringbootTest 加在类上,会启动整个环境;做集成测试;由于controller层逻辑少,一般用在controller层;比较慢
    • @ExtendWith(SpringExtension.class) Junit5中用来替代Junit4的@RunWith(SpringJUnit4ClassRunner.class),会启动Spring的上下文
    • @ContextConfiguration 指定加载ApplicationContext的配置文件或配置类,一般和@ExtendWith(SpringExtension.class)结合使用
    • @ExtendWith(MockitoExtension.class) Junit5中用来替代JUnit4 @RunWith(MockitoJUnitRunner.class)。不需要Spring上下文时用该注解
  • 模拟类或接口的注解

    • @Mock、@InjectMocks、@Spy Mockito库提供的注解;@Mock创建一个Mock对象,@InjectMocks创建一个实例,尝试将其它有@Mock或@Spy的对象注入到该实例中;@Spy默认会调用真实的方法,@Mock默认不执行
    • @MockBean、@SpyBean Spring Boot包装Mockito库提供的注解;@MockBean创建的对象默认会加入Spring的上下文中,如果Spring的上下文存在则替换;@MockBean和@SpyBean区别同@Mock和@Spy的区别
  • 方法注解

    • Test 同Junit4
  • 常用测试方法

    • Mockito.when().thenReturn()
    • Mockito.doNothing().when(类).方法
    • Assertions.assertTrue
    • Assertions.assertNotNull
    • Mockito.verify(bean, times(次数)).方法
    • Assertions.assertThrows(Class expectedType, () -> 类.方法)

三. 测试方法结构

单元测试采用Given...When...Then的结构,即

  1. 准备数据,用mock模拟方法返回值
  2. 执行,调用测试方法
  3. 验证,用assert等验证方法返回结果

四. 数据库的单元测试

@MybatisPlusTest

只注入MybatisPlus相关Bean做单元测试,比较像@Mybatis注解,由mybatis-plus的3.4.0版本后的mybatis-plus-boot-starter-test模块提供。

可以在做数据库的单元测试时不使用@SpringBootTest注解启动整个工程

五. 接入层的单元测试

@WebMvcTest

同样用来做MVC层的单元测试,只注入MVC层相关的Bean

六. demo

  • pom依赖
<dependency>
  <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter-test</artifactId>
  <version>3.4.3</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>
  • dao层单元测试
@MybatisPlusTest
@ContextConfiguration(classes = { JunitDemo.class })
class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void should_select5Items_when_afterInit() {
        List<User> users = userMapper.selectList(null);
        Assertions.assertEquals(5, users.size());
    }
}
  • service层单元测试
@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserMapper userMapper;

    @InjectMocks
    private UserService userService;

    @Test
    void should_returnResult_when_queryById() {
        // Given 准备数据:初始状态或前置条件
        long userId = 1;
        User mockUser = new User();
        mockUser.setId(userId);
        mockUser.setName("li");
        Mockito.doReturn(mockUser).when(userMapper).selectById(userId);

        // When 执行:行为发生
        User user = userService.getOnlyUser(0);

        // Then 验证:断言结果
        Mockito.verify(userMapper, Mockito.times(1)).selectById(userId);
        Assertions.assertEquals("li", user.getName());
    }

    /**
     * 小于18岁入库抛出异常
     */
    @Test
    void should_businessException_when_ageLessThan18() {
        Mockito.doNothing().when(userMapper).insert(Mockito.isA(User.class));
        Assertions.assertThrows(BusinessException.class, () -> userService.insertUser(buildUserAgeLessThan18()));
    }
}
  • RestController的集成测试
@SpringBootTest(classes = JunitDemo.class)
@AutoConfigureMockMvc
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void should_returnUser_when_query() throws Exception {
        mockMvc.perform(
                MockMvcRequestBuilders.get("/user/getOnlyUser?userId=1").contentType(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("name").value("xiaoming"))
                .andExpect(MockMvcResultMatchers.jsonPath("age").value("18")).andReturn();
    }

}