单元测试 springboot + junit5

引言

开发人员使用单元测试的手段检查代码质量,其主要思路在于构造虚假的依赖方法返回值,组织用例检查局部逻辑处理后的返回值是否符合预期。搭建 springboot + junit5 单元测试框架简单易行,能够处理常见的静态方法和抛出异常。

依赖引入

spring-boot-starter-test 支持 junit5 框架,jupiter 作为 junit5 依赖,可以导入 @Extend、@Test 等注解和 Assertions 等方法,mockito-core 作为构造依赖,可以导入 @InjectMocks、@Mock 等注解和 Mockito 等方法。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.1.14.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <scope>test</scope>
</dependency>
复制代码

controller 层单元测试

接口层测试借助 mockmvc.perform 构造 get/post 请求,单元测试会构造 service 方法返回值,但这一层通常没有太多执行逻辑,测试意义不大。

public class ControllerTest {

    private MockMvc mockMvc;

    @InjectMocks
    private Controller controller;

    @Mock
    private Service service;

    @BeforeEach
    void init() {
        MockitoAnnotations.openMocks(this);
        mockMvc = MockMvcBuilders.standaloneSetup((Controller)).build();
    }

    @Test
    @SneakyThrows
    void executeTest() {
        // mock request
        Request req;

        // mock service
        when(service.execute(param)).thenReturn(mock_result);

        MockHttpServletResponse response = mockMvc.perform(
                        MockMvcRequestBuilders.post("/execute")
                                .content(JSONObject.toJSONString(req))
                                .contentType(MediaType.APPLICATION_JSON)
                )
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andReturn().getResponse();

        assertTrue(response.getContentAsString().contains(mock_result));
    }

    @Test
    @SneakyThrows
    public void queryTest() {
        // mock service
        when(service.query(param)).thenReturn(mock_result);

        MockHttpServletResponse response = mockMvc.perform(
                        MockMvcRequestBuilders.get("/query")
                                .param("param_name1", param1)
                                .param("param_name2", param2)
                )
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andReturn().getResponse();

        assertTrue(response.getContentAsString().contains(mock_result));
    }
}
复制代码

@SneakyThrows 注解用于处理 mockmvc.perform 可能抛出的异常。post 请求用 content 和 contentType 处理入参对象,get 请求用 param 处理多个入参。

service 层单元测试

服务层借助 mockito.when().thenReturn() 构造其他 manager/service 和 mapper 的返回值,单元测试的执行逻辑主要集中在这一层。

@ExtendWith(MockitoExtension.class)
class ServiceImplTest {

    @InjectMocks
    private ServiceImpl service;

    @Mock
    private Manager manager;

    @Mock
    private AnotherService anotherService;

    @Mock
    private Mapper mapper;

    @Test
    void queryTest() {
        // mock manager
        when(manager.get(param)).thenReturn(mock_manager_result);
        
        // mock anotherService
        when(anotherService.get(param)).thenReturn(mock_service_result);
        
         // mock mapper
        when(mapper.selece(param)).thenReturn(mock_mapper_result);

        assertEquals(service.query(param), prospective_result);
    }
}
复制代码

静态方法

如果待测试的方法中调用了静态方法,需要在测试方法执行前构造这个静态方法。

@BeforeAll
void setup(){
    // mock static method
    MockedStatic<Utils> utilsMockedStatic = mockStatic(Utils.class);
    utilsMockedStatic.when((MockedStatic.Verification) Utils.generate(param)).thenReturn(mock_result);
}
复制代码

如果一个测试类里,多个测试方法都调用同一个静态类,就需要在测试类前添加 @TestInstance(PER_CLASS) 注解,确保每个测试方法重新启动,否则会有报错。当返回值是 String 类型时,MockedStatic.Verification 不能处理,还是在测试方法里用 when(Utils.get(param)).thenReturn(mock_string_result) 的方式处理。

静态参数

如果待测试的方法中定义了静态参数(通常是 @Value 传入的值),需要借助反射把构造的值传进去。

public void init() throws IllegalAccessException, NoSuchFieldException {
    // reflect @Value
    Field field = ServiceImpl.class.getDeclaredField("static_param_name");
    field.setAccessible(true);
    field.set(service, mock_value);
}
复制代码

工厂创建对象/接口

如果待测试的方法内执行了工厂类构造新对象的操作,或者产生了接口,需要构造两层返回时,可以通过 Mockito.mock 处理。

Interface interface = mock(Interface.class);
when(service.getInterface(param)).thenReturn(interface);
when(interface.query(param)).thenReturn(mock_interface_result);
复制代码

异常构造

如果待测试的方法有抛出异常的分支,junit5 使用 assertThrows 方法封装这些处理,进行简单的异常情况测试。

Exception exception = assertThrows(Exception.class, () -> {
    service.get(param);
});
assertEquals(exception.getErrorMessage().getDisplayedMessage(), "非法参数");
复制代码

枚举构造

如果待测试的方法有对于枚举值的判断,需要添加一个新的枚举值来触发异常时,类似处理静态方法,但是需要把原有的枚举值添加进去。

try (MockedStatic<Enum> mockedDropEnum = mockStatic(Enum.class)) {
    Enum mockEnum = mock(Enum.class);
    mockedDropEnum.when(Enum::values).thenReturn(new Enum[]{
            Enum.ORIGINAL_ENUM, mockEnum});

    Exception exception = assertThrows(Exception.class, () -> {
        service.check(mockEnum);
    });
    
    assertEquals(exception.getErrorMessage().getCode(), "异常类型");
}
复制代码

总结

总上,springboot + junit5 框架提供了丰富的单元测试方法,基本满足 java 开发中日常的单元测试需求。最后,单元测试的目标是提升代码质量,发现写代码时没注意到的问题,不要陷入到细节里,也不要过分在意覆盖率,要真实地构造用例,分析和优化代码。

猜你喜欢

转载自juejin.im/post/7118698328044503071