概述
单元测试通过屏蔽复杂的依赖关系,集中于某个具体的类的逻辑进行测试,可以有效在代码编写初期发现逻辑问题。本文使用Mockito技术实现全部场景下单元测试编写,配合集成测试能够达到更好的效果。
单元测试编写有如下要求:
1、单元测试覆盖率要达到100%。
2、不引入额外的,不需要的类。
依赖引入
<!--测试依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>junit</artifactId>
<groupId>junit</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>junit-jupiter</artifactId>
<exclusions>
<exclusion>
<artifactId>mockito-core</artifactId>
<groupId>org.mockito</groupId>
</exclusion>
<exclusion>
<artifactId>junit-jupiter-api</artifactId>
<groupId>org.junit.jupiter</groupId>
</exclusion>
</exclusions>
<version>2.20.0</version>
</dependency>
<!--mockito的依赖项-->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.4.0</version>
<exclusions>
<exclusion>
<artifactId>mockito-core</artifactId>
<groupId>org.mockito</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<exclusions>
<exclusion>
<artifactId>byte-buddy</artifactId>
<groupId>net.bytebuddy</groupId>
</exclusion>
<exclusion>
<artifactId>byte-buddy-agent</artifactId>
<groupId>net.bytebuddy</groupId>
</exclusion>
</exclusions>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.12.9</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.12.9</version>
<scope>test</scope>
</dependency>
<!--mybatisplus测试为仅加载mybatisplus引入的依赖项-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter-test</artifactId>
<version>3.5.2</version>
<scope>test</scope>
</dependency>
<!--数据库测试引入的内嵌数据库-->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
<scope>test</scope>
</dependency>
<!--pojo geter seter方法引入的组件-->
<dependency>
<groupId>com.openpojo</groupId>
<artifactId>openpojo</artifactId>
<version>0.9.1</version>
</dependency>
<!--本地查看单元测试进度引入的组件-->
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
</dependency>
测试场景
普通业务代码
由于 Spring 技术的广泛使用,大部分服务内的类都是交由 Spring 容器进行管理的,一个类往往依赖很多其他类,在编写单元测试时,屏蔽其他类是一个十分麻烦的过程。使用 Mocikito 可以使用类似于 Spring 依赖注入的语法,减少工作量。
/**
* @ExtendWith注解使用MockitoExtension,会识别@InjectMocks和@Mock等注解
* @MockitoSetting注解,设置为Strictness.LENIENT,忽略没被使用的mock方法,避免重复代码
*/
@ExtendWith({MockitoExtension.class})
@MockitoSettings(strictness = Strictness.LENIENT)
public MockitoTest {
/**
* @InjectMocks注解的对象是需要测试的类
* @Mock注解的对象会被加入到context中,自动组装给@InjectMocks注解的对象
*/
@InjectMocks
private TestClass testClass;
@Mock
private DependencyClass dependencyClass;
/**
* 在每个测试方法前进行初始化
*/
@BeforeEach
public void init() {
Mockito.doAnswer(invocation -> {
Object param = invocation.getArgument(0);
return "";
}).when(testClass).test(Mockito.any());
}
/**
* 执行具体测试场景
*/
@Test
public void test() {
testClass.test("test")
}
}
使用静态方法的业务
在测试类中往往会存在调用了其他类静态方法的情况,有时静态方法的返回值会影响测试流程,所以需要对静态方法的返回值进行设定。对静态方法类的 mock 不能使用 @Mock 注解完成,需要额外定义一个 MockedStatic 对象处理,需要注意的是,静态类的 mock 的作用域是当前线程。
/**
* 对需要进行 mock 的静态类进行声明
*/
MockedStatic<StaticClass> staticClass;
@BeforeEach
public void init() {
staticClass= Mockito.mockStatic(StaticClass.class);
staticClass.when(() -> StaticClass.staticMethod()).thenReturn("10");
}
/**
* 当前线程下所有单元测试执行完成后,需要将静态 mock 资源释放
*/
@AfterEach
public void after() {
staticClass.close();
}
使用特定对象的业务
在具体的测试方法中,往往会存在使用构造器创建新对象的情况,后续还会调用此对象的特定方法。在单元测试中,这种在方法内直接创建的对象往往不受我们控制,如果需要在对象创建时返回一个我们定义好的 mock 对象,可以使用构造器 mock 对象。
/**
* 对需要进行 mock 的类进行声明
*/
MockedConstruction<ConstructionClass> constructionClass;
@BeforeEach
public void init() {
constructionClass= Mockito.mockConstruction(constructionClass.class, (mock, context) -> {
doAnswer(invocation -> {
return null;
}).when(mock).test();
});
}
/**
* 当前线程下所有单元测试执行完成后,需要将构造器 mock 资源释放
*/
@AfterEach
public void after() {
constructionClass.close();
}
controller 层接口单元测试
controller 层涉及到 web 服务的启动,很多拦截器相关的逻辑都需要加载 web 环境才能实现,所以尽量在保证引入最小依赖的前提下,完成接口测试。
/**
* @ExtendWith({SpringExtension.class}) 使用 SpringExtension 来管理容器
* @WebMvcTest 只加载测试 controller 依赖的对象
* @ContextConfiguration 指定启动类
* @MockBeans 指定 mock 类
*/
@ExtendWith({SpringExtension.class})
@WebMvcTest(controllers = TestController.class)
@ContextConfiguration(classes={ApplicationTest.class})
@MockBeans({@MockBean(TestService.class)})
public class ControllerTest {
/**
* 装配 MVC 环境
*/
@Autowired
private MockMvc mockMvc;
@Autowired
TestService testService;
@Test
public void controllerTest() {
TestRequest requset = new TestRequest ();
String url = "/test";
//存在则成功
ResultActions resultAction = this.mockMvc.perform(post(url)
.content(JSONObject.toJSONString(requset))
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andDo(print());
resultAction.andExpect(status().isOk())
.andExpect(content().string(containsString("处理成功")));
}
}
同一场景多值单元测试
我们在编写单元测试时,往往会出现在同一个场景下需要测试不同值的情况,比如在测试“字符串转整型”功能时,需要测试字符串为空、字符串非空、字符串非数字等情况。出了传值有区别外,其他场景完全一致。为了简化单元测试编写,可以使用 @ParameterizedTest 和 @ValueSource 注解,完成多值测试。
@ParameterizedTest
@ValueSource(strings = {null, "hello", "1"})
void testStringToInt(String str) {
Integer.parseInt(str);
}
总结
Mockito 是一个功能十分强大的单元测试框架。灵活搭配上述场景中描述的不同的特性,几乎可以覆盖全部的单元测试需求。