引言
开发人员使用单元测试的手段检查代码质量,其主要思路在于构造虚假的依赖方法返回值,组织用例检查局部逻辑处理后的返回值是否符合预期。搭建 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 开发中日常的单元测试需求。最后,单元测试的目标是提升代码质量,发现写代码时没注意到的问题,不要陷入到细节里,也不要过分在意覆盖率,要真实地构造用例,分析和优化代码。