1. 概述
Mockito是一个用于Java单元测试的优秀强大的框架,当需要调用第三方接口而开发测试环境又无法直接调用此接口时,就可以使用Mockito模拟接口调用编写完美的单元测试,这样也使得与第三方应用进行了强解耦,更多详情请参阅Mockito官网
2. 引入Mockito依赖
由于SpringBoot自身整合了Mockito,所以在整合Mockito编写单元测试的时候,只需要引入test依赖即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
另外,Mockito在junit4和junit5中使用的注解有些区别,如果是使用junit4进行单元测试,还需引入junit4的依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
3. 接口代码编写
本文使用SpringBoot2.3.12和MyBatis-Plus3.4.2编写Dao层和Service层接口,这里不做详细讲解,有不清楚如何使用的小伙伴请参阅SpringBoot2.3.4整合MyBatis-Plus3.4.0和Swagger3.0,此外,还会用到mapstruct,具体使用方法可参阅SpringBoot2.3整合MapStruct实现Java bean映射
4. 编写测试类
4.1. 测试类中引入mock
在测试类中引入mock有两种方法,一种是在代码中导入静态方法mock,另一种是使用注解@Mock。官网推荐使用注解方式引入mock,其优点如下:
- 最大限度地减少重复的模拟创建代码
- 使测试类更具可读性
- 使验证错误更易于阅读,因为字段名称用于标识模拟
注意:使用@Mock注解时,需要添加运行器使注解生效
junit4中有三种方法,具体如下:
- 初始化mock:MockitoAnnotations.initMocks(this)
- 测试类上使用注解MockitoJUnitRunner
- 使用MockitoRule
示例代码如下:
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@RunWith(MockitoJUnitRunner.StrictStubs.class)
public class SysUserInfoServiceJunitRunnerTest {}
@Rule
public MockitoRule rule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
其中MockitoJUnitRunner有三种方式
- Silent:实现忽略存根参数不匹配 (MockitoJUnitRunner.StrictStubs) 并且不检测未使用的存根
- Strict:检测未使用的存根并将它们报告为失败
- StrictStubs:改进调试测试,帮助保持测试干净
MockitoRule有两种方式
- silent():规则不会在测试执行期间报告存根警告
- strictness(Strictness strictness):严格级别,尤其是“严格存根”(Strictness.STRICT_STUBS)有助于调试和保持测试清洁,另外还有严格级别LENIENT和WARN
junit5中也有三种方法,具体如下:
- 初始化mock:MockitoAnnotations.initMocks(this)
- 测试类上使用注解@ExtendWith(MockitoExtension.class)
- 测试类上使用注解@MockitoSettings,为测试类配置使用的严格等级
示例代码如下:
@BeforeEach
void setUp() {
MockitoAnnotations.initMocks(this);
}
@ExtendWith(MockitoExtension.class)
public class SysUserInfoServiceExtensionTest {}
@MockitoSettings(strictness = Strictness.STRICT_STUBS)
public class SysUserInfoServiceSettingsTest {}
4.2. 常用注解
mock创建的注解有
@Mock:用于创建和注入模拟实例
@Spy:监视现有实例
@InjectMocks:将模拟字段自动注入到测试对象中
@Captor:用于获取参数
@Mock与@Spy的区别
- 使用@Mock创建一个mock时,是从类型的类中创建的,而不是从实际实例中创建,mock只是创建了一个类的基本外壳实例,用于跟踪与其的交互。
- 使用@Spy时,将包装一个现有的实例,除了行为方式与普通实例相同外,还将检测以跟踪与它的所有交互。
- 通常情况下@Mock用于访问第三方服务接口,@Spy用于访问本服务接口(读取配置类或者mapstruct接口)
实例代码如下:
class SysUserInfoServiceSpyTest {
@Mock
private SysUserInfoMapper userInfoMapper;
@Spy
private final UserInfoMapper infoMapper = new UserInfoMapperImpl();
@InjectMocks
private SysUserInfoServiceImpl userInfoService;
@BeforeEach
void setUp() {
MockitoAnnotations.initMocks(this);
}
}
4.3. Stubs(存根/打桩)
使用when().thenReturn()存根有返回值的接口,使用when().thenThrow()存根有异常的接口,使用when().thenAnswer()存根有回调函数的接口,使用given().willReturn()存根BDD格式有返回值接口,第一个()中是mock对象的方法调用,第二个()中是返回的对象。
两种的功能都是一样的,given()是行为驱动开发BDD(Behavior Driven Development)的风格格式。
示例代码如下:
@Test
void getUserInfoByIdTest() {
final SysUserInfo userInfo = SysUserInfo.builder()
.id(1L)
.userName("admin")
.password("123456")
.sex(2)
.age(99)
.email("admin@163.com")
.createUser("admin")
.createTime(LocalDateTime.now())
.updateUser("admin")
.updateTime(LocalDateTime.now())
.build();
Mockito.when(userInfoMapper.selectById(any())).thenReturn(userInfo);
// 或者
// BDDMockito.given(userInfoMapper.selectById(any())).willReturn(userInfo);
final SysUserInfo info = userInfoService.getById(1);
Assertions.assertNotNull(info);
}
当以下情况时,使用带do的方法
- 存根void方法
- 在spy对象上存根方法
- 多次存根相同的方法,以在测试过程中更改模拟的行为
常用的带do的方法有doThrow()、doAnswer()、doNothing()、doReturn()和doCallRealMethod()
doReturn():用于当监视真实对象并在spy上调用真实方法时会带来副作用,或覆盖先前的异常存根
doThrow():用于给void方法存根需要有异常抛出
doAnswer():用于给void方法存根需要有回调值
doNothing():用于给void方法存根不需要做任何事,使用情况为对void方法连续调用进行存根或者监视真实对象且void方法不执行任何操作
doCallRealMethod():用于调用真正执行的方法
4.4. 验证
verify
用于验证某些行为至少发生过一次,例如:
verify(userInfoMapper).selectById(anyInt());
或者
verify(userInfoMapper, times(1)).selectById(anyInt());
验证某些行为发生了至少一次/确切的次数/从未使用
atLeastOnce():至少发生一次
verify(userInfoMapper, atLeastOnce()).selectById(anyInt());
atLeast(num):至少发生num次
verify(userInfoMapper, atLeast(1)).selectById(anyInt());
atMostOnce():最多发生一次
verify(userInfoMapper, atMostOnce()).selectById(anyInt());
atMost(num):最多发生num次
verify(userInfoMapper, atMost(1)).selectById(anyInt());
never():从未发生过
verify(userInfoMapper, never()).selectOne(any());
only():校验的方法是否是唯一调用
verify(userInfoMapper, only()).selectById(anyInt());
timeout():给定的时间(毫秒)内一直触发验证,可以用于测试异步代码
verify(userInfoMapper, timeout(100)).selectById(anyInt());
verify(userInfoMapper, timeout(100).times(1)).selectById(anyInt());
after():给定的时间(毫秒)后触发验证,可以测试异步代码
verify(userInfoMapper, after(100)).selectById(anyInt());
verify(userInfoMapper, after(100).times(1)).selectById(anyInt());
timeout()与after()区别
- timeout()验证通过后立即成功退出
- after()等待给定的时间后才开始验证
ArgumentCaptor
用于获取请求参数以进行进一步断言,通常结合verify()一起使用,其适用条件如下:
- 自定义参数匹配器不能被重用
- 只需要对参数值进行断言即可验证
测试类中引入ArgumentCaptor可以使用注解@Captor,也可以使用
ArgumentCaptor<Class> argumentCaptor = ArgumentCaptor.forClass(Class.class);
其主要方法有capture()、getValue()、getAllValues()
capture():用于捕获参数值,必须在验证内部使用此方法
verify(userInfoMapper, times(1)).insert(argumentCaptor.capture());
getValue():返回捕获的参数值,如果验证方法被多次调用,只返回最新捕获的值
assertEquals("admin", argumentCaptor.getValue().getUserName());
getAllValues():返回捕获的所有参数值,用于捕获可变参数或多次调用验证方法,当多次调用varargs方法时,返回来自所有调用的所有值的合并列表
InOrder
用于按顺序验证mock对象,可以只验证需要的mock对象
final InOrder inOrder = inOrder(userInfoMapper, infoMapper);
inOrder.verify(userInfoMapper).selectById(anyInt());
inOrder.verify(infoMapper).map(any());
calls():允许按顺序进行非贪婪验证
inOrder.verify(userInfoMapper, calls(1)).selectById(anyInt());
与times(1)不同的是,如果该方法调用了2次也不会报错
与atLeast(1)不同的是,不会将第二次标记为已验证
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/76782.html