【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>
前言
在多个成员协同开发的时候,有没有遇到过类似这样的情况:
A成员在开发某个接口但是没有完成,而B恰好依赖于A,在不了解Mockito的情况下只能等待A完成之后才可以自测,完全受制于他人,不知你是否想过怎么摆脱这样一个尴尬的场景,下面咱们就根据这样一个场景来做具体讲述。
目录
- 什么是Mockito?
- 为什么需要模拟?
- 有哪些需要注意的关键点?
- 如何Mock?
- 结合Junit使用
- 注意事项
正文
1. 什么是Mockito?
Mockito是一个单元测试框架,封装了简洁易用的API,对于新手来说也可以很容易的入门,同时在我们编写单元测试的时候还可以以锻炼我们的逻辑思维能力。
2. 为什么需要模拟?
原因一: 在我们实际程序开发中经常遇到下图这种类与类之间的层级依赖:
正如前言所说,假设ImageService还未开发完毕,此时我们的开发将受限于他人
原因二: 在我们写单元测试的时候,业务层的数据大多数人都是从数据库读取,假设某位同事把数据不小心删除了,那我们的单元测试就有可能出问题。专业的说法就是:数据与应用隔离
原因三: 锻炼我们的逻辑思维能力,为什么这么说呢?在我们写Mock单元测试的时候,你必须了解你的逻辑,对你的逻辑做一些验证,比如说测试A服务的时候调用B,需要在调用B服务的时候做参数抓取校验,参数的值必须是XX,调用完毕的结果必须是XX等等;这些都是为了验证我们逻辑的正确性,从而提高我们的逻辑思维能力。
3. 哪些需要注意的关键点?
- mock数据:在测试某个服务的时候,需要提前把这个依赖的接口mock 数据,一般都是使用注解:@mock 、 @InjectMocks
- 设置预期:由于依赖其他小伙伴的接口,所以我们可以在调用接口的时候设置预期的参数以及返回值,在程序执行的时候mockito自动就会帮我们返回预计设置的值(注意这里不会真正调用依赖的接口)。当然这个只是其中一种设置预期,比如说我们还可以设置某个方法调用的次数等信息。
- 验证结果:一般来说设置预期与设置验证结果都是一起的。如果在一个单元测试中,如果你设定的预期没有得到满足,那么这个单元测试就是失败的。例如设置预期结果是调用userService.save的时候用户名必须走风控,头像必须上传到第三方服务器上,但是在测试中它并没有被调用,那么测试就失败了。
4. 如何Mock?
- 4.1 模拟对象
我们创建mock依赖对象一般都是使用注解:@mock 、 @InjectMocks ,那这两个有什么区别呢?
@mock: 由Mockito框架根据接口的实现帮我们mock一个虚假对象,该对象的方法不会真正去执行
@InjectMocks: 由Mockito框架根据接口的实现帮我们创建一个实例对象(注意是实例对象,所以调用该类的方法都会被执行),同时会根据@Mock的类与@InjectMocks标识的类中的属性类型进行匹配,如果完全一致,自动注入属性值,类似于Spring的IOC功能,之前不知道,还是自己手写的工具类反射赋值的【蠢哭】
@Mock
private ImageSercice imageSercice;
@Mock
private RiskSercice riskSercice;
@InjectMocks
private UserSercice userSercice;
- 4.2 设置预期
首先介绍下我们常用的静态方法:
4.2.1 Mockito.mock: 创建一个模拟类实例对象,与@mock的作用是一样的,注意不是@InjectMocks,想要注入依赖对象还是老老实实的使用@InjectMocks吧。当我们使用这个方法创建出来的对象,结合when().thenReturn()时候,调用的方法是不会执行的。
4.2.2 Mockito.when: 用于调用某个方法的时候设置期望,具体请看如下DEMO,设置了当调用imageService.upload(参数)时返回true,调用riskService.risk(参数)时返回new HashMap():
@Test
public void testWhen() {
Mockito.when(imageService.upload(Mockito.any())).thenReturn(true);
Mockito.when(riskService.risk(Mockito.anyMap())).thenReturn(new HashMap());
userService.save(new HashMap<>());
}
4.2.3 Mockito.spy::与mock类似,但他创建的是实例对象,所以调用其方法会被真实执行的,结合when().thenReturn()时候看下图DEMO所示:
4.2.4 Mockito.doThrow: 校验方法出现异常时的校验,
// 保存用户的时候抛出Exception异常
Mockito.doThrow(new NullPointerException()).when(Mockito.mock(UserService.class)).save(null);
4.2.5 Mockito.eq: 一般在设置Mockito.when参数值时使用,判断参数等于预期值,如果不相等则设置的 when 无效,如果不写的话,默认就是eq;但是如果一个方法有多个参数且不同类型,做参数值判断的时候Mockito.eq 必须写,否则运行值会报错
Mockito.when(userService.getById(1)).thenReturn(userModel);
// 当单个参数是,可以改写成
Mockito.when(userService.getById(Mockito.eq(1))).thenReturn(userModel);
// 多个参数,校验的Mockito参数类型参数不同,此种情况每个参数都必须添加特定类型参数值校验,
// 示例demo:userService.get(age,name,date)
// 错误写法
Mockito.when(userService.get(1,null,new Mockito.isA(Date.class))).thenReturn(userModel);
// 正确写法
Mockito.when(userService.get(Mockito.eq(1),Mockito.isNull(),Mockito.isA(Date.class))).thenReturn(userModel);
// 也是正确写法,但是new Date 与程序中的new Date是两个对象,不会命中预设值的Mockito.when(userService.get(1,null,new Date())).thenReturn(userModel);
Mockito.when(userService.get(1,null,new Date())).thenReturn(userModel);
4.2.6 Mockito.any: 返回一个对象,支持Null,一般是在mockito.when() 被调用的方法中使用,表示忽略对象值
// 与 Mockito.eq 中的demo类似 ,只是将Mockito.isA 换成了any
Mockito.when(userService.get(Mockito.eq(1),Mockito.isNull(),Mockito.any(Date.class))).thenReturn(userModel);
4.2.7 Mockito.isNull: 返回一个空对象,一般是在mockito.when() 被调用的方法中使用
// 与 Mockito.any 中的demo类似
Mockito.when(userService.get(Mockito.eq(1),Mockito.isNull(),Mockito.any(Date.class))).thenReturn(userModel);
4.2.8 Mockito.isA: 返回一个指定类型对象与any 类似
// 与 Mockito.any 中的demo类似
Mockito.when(userService.get(Mockito.eq(1),Mockito.isNull(),Mockito.any(Date.class))).thenReturn(userModel);
4.2.9 Mockito.doNothing(): 只是声明一个Mock方法,基本上它告诉Mockito在调用模拟对象中的方法时什么也不做。有时用于void返回方法或没有副作用的方法,或者与正在进行的单元测试无关
- 4.3 验证结果 4.3.1 Mockito.verify: 校验方法,传入不同的校验规则做不同的校验,如下示例为校验某个方法被调用的次数校验
// 校验 riskService.risk 方法被调用一次
Mockito.verify(riskService, Mockito.times(1)).risk(Mockito.any(RiskModel.class));
4.3.2 Mockito.times: 对调用次数的校验,不单独使用,需结合Mockito.verify() 方法
// 校验 riskService.risk 方法被调用一次:Mockito.times(次数)
Mockito.verify(riskService, Mockito.times(1)).risk(Mockito.any(RiskModel.class));
4.3.3 Mockito.never: 表示一个方法从来没有调用过,与Mockito.times(0)一样
4.3.4 Mockito.reset: 重置Mock的对象的相关联引用
userService.save(userModel);
// 验证通过
Mockito.verify(riskService, Mockito.times(1)).risk(Mockito.any(RiskModel.class));
// 重置已经统计的次数
Mockito.reset(userService);
userService.save(userModel);
// 验证通过
Mockito.verify(riskService, Mockito.times(1)).risk(Mockito.any(RiskModel.class));
4.3.5 Mockito.atLeastOnce: 表示某个方法至少调用 X 次
4.3.6 Mockito.atMost: 表示某个方法最多调用 X 次
4.3.7 Mockito.timeout: 调用某个方法超时时间
4.3.8 Mockito.validateMockitoUsage:
5. 结合Junit使用
- 加载Mock入口:
在使用Junit的时候,如果不制定加载类的Runner,默认使用Junit自己的运行方法。当然,我们在使用Mock的时候,为了结合Junit,我们需要手动制定Runner ,即:@RunWith(MockitoJUnitRunner.class) 或者在@Before方法中设置 MockitoAnnotations.initMocks(this); 两者的作用是等价的
- 示例demo
// 图片服务
public class RiskService {
public Map<String, List<String>> risk(Map<String,Object> param){
System.out.println("riskService");
return null;
}
}
// 风控服务
public class RiskService {
public Map<String, List<String>> risk(Map<String,Object> param){
System.out.println("riskService");
return null;
}
}
// 用户服务
public class UserService {
private ImageService imageService;
private RiskService riskService;
public Map<String, String> save(Map<String, Object> user) {
Map<String, String> resultMap = new HashMap<>();
Map<String, List<String>> riskMap = riskService.risk(user);
if (!CollectionUtils.isEmpty(riskMap)) {
for (Map.Entry<String, List<String>> entry : riskMap.entrySet()) {
resultMap.put(entry.getKey(), StringUtils.join(entry.getValue(), ","));
}
return resultMap;
}
boolean flag = imageService.upload((InputStream) user.get("headImage"));
if (flag) {
resultMap.put("headImage", "上传头像失败请稍后重试");
return resultMap;
}
// 插入数据库等其他逻辑
return resultMap;
}
}
// 测试类
@RunWith(MockitoJUnitRunner.class)
public class MockitoTest {
@Mock
private RiskService riskService;
@Mock
private ImageService imageService;
@InjectMocks
// @Mock
private UserService userService;
@Before
public void init() {
// 在不使用 @RunWith(MockitoJUnitRunner.class) 的时候需要手动扫描注解
// MockitoAnnotations.initMocks(this);
// // 在不使用 @InjectMocks 的时候需要反射赋值,但是这样赋值测试的时候经常会出现一些问题,建议使用 @InjectMocks
// setField(userService,"riskService",riskService);
// setField(userService,"imageService",imageService);
printLog("userService:", userService, ",riskService:", riskService, ",imageService:", imageService);
}
@After
public void after() {
printLog("执行成功");
}
@Test
public void testWhen() {
Mockito.when(imageService.upload(Mockito.any())).thenReturn(true);
Mockito.when(riskService.risk(Mockito.anyMap())).thenReturn(new HashMap());
userService.save(new HashMap<>());
}
@Test
public void testVerify() {
testWhen();
Mockito.verify(imageService, Mockito.times(1)).upload(Mockito.any());
}
@Test
public void testInOrder() {
// 正确写法
testWhen();
final InOrder inOrder = Mockito.inOrder(riskService, imageService);
inOrder.verify(riskService).risk(Mockito.anyMap());
inOrder.verify(imageService).upload(Mockito.any());
}
@Test
public void testAtLeastOnce() {
testWhen();
testWhen();
Mockito.verify(riskService, Mockito.atLeastOnce()).risk(Mockito.anyMap());
Mockito.verify(imageService, Mockito.atLeastOnce()).upload(Mockito.any());
}
@Test
public void testNever() {
// 正确执行
Mockito.verify(riskService, Mockito.never()).risk(Mockito.anyMap());
// 设置引用
// testWhen();
// 执行错误
Mockito.verify(riskService, Mockito.never()).risk(Mockito.anyMap());
}
@Test
public void testArgumentCaptor() {
// 正常请求
Map<String, Object> param = new HashMap<>();
param.put("headImage", Mockito.any(InputStream.class));
userService.save(param);
// 对上面执行的参数进行抓取
ArgumentCaptor<Map> captor = ArgumentCaptor.forClass(Map.class);
Mockito.verify(riskService).risk(captor.capture());
Assert.assertEquals(captor.getValue(), param);
}
@Test
public void testVerifyNoMoreInteractions() {
UserService userService = Mockito.mock(UserService.class);
// 正确执行
Mockito.verifyNoMoreInteractions(userService);
// 重置引用
Mockito.reset(userService);
// 正确执行
Mockito.verifyNoMoreInteractions(userService);
// // 设置引用
// userService.save(Mockito.anyMap());
// // 执行这里会报错
// Mockito.verifyNoMoreInteractions(userService);
}
@Test
public void testDoExcption() {
Mockito.doThrow(new NullPointerException()).when(Mockito.mock(UserService.class)).save(null);
}
@Test
public void testa(){
Map<String, Object> param = new HashMap<>();
param.put("headImage", Mockito.any(InputStream.class));
userService.save(param);
}
/**
* 打印日志
*
* @param objs
*/
private void printLog(Object... objs) {
if (objs == null && objs.length == 0) {
System.out.println("参数为空,没有什么东西可以打印的");
}
StringBuilder sb = new StringBuilder();
Arrays.stream(objs).forEach(obj -> sb.append(obj));
System.out.println(sb.toString());
}
/**
* 反射赋值
*
* @param t 实例对象
* @param fieldName 实例对象的属性值
* @param o 待赋的值
* @param <T> T 类型
* @return 赋值之后的实例对象
*/
public static <T> T setField(T t, String fieldName, Object o) {
if (StringUtils.isBlank(fieldName)) {
return t;
}
try {
Class<?> clsObj = t.getClass();
final Field[] childFields = clsObj.getDeclaredFields();
final Optional<Field> childOptionalField = Arrays.asList(childFields).stream().filter(childField -> childField.getName().equals(fieldName)).findFirst();
if (childOptionalField.isPresent()) {
Field f = clsObj.getDeclaredField(fieldName);
f.setAccessible(true);
f.set(t, o);
return t;
}
// 如果子类没有属性,看看父类是否有该属性
if (clsObj.getSuperclass() == null) {
return t;
}
T parentObj = (T) clsObj.getSuperclass().newInstance();
// 递归赋值咯
parentObj = setField(parentObj, fieldName, o);
// 拷贝到子类中,然后返回子类
copyPropertiesIgnoreNull(t, parentObj);
return t;
} catch (Exception e) {
e.printStackTrace();
}
return t;
}
private static void copyPropertiesIgnoreNull(Object src, Object target) {
BeanUtils.copyProperties(src, target, getNullPropertyNames(src));
}
/**
* 获取空值属性列表
*
* @param source
* @return
*/
private static String[] getNullPropertyNames(Object source) {
final BeanWrapper src = new BeanWrapperImpl(source);
java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors();
Set<String> emptyNames = new HashSet<>();
for (java.beans.PropertyDescriptor pd : pds) {
// 忽略部分字段
if (Arrays.asList("callback", "callbacks", "classes").contains(pd.getName())) {
continue;
}
Object srcValue = src.getPropertyValue(pd.getName());
if (srcValue == null) {
emptyNames.add(pd.getName());
}
}
String[] result = new String[emptyNames.size()];
return emptyNames.toArray(result);
}
}
6. 注意事项
总结
来源:oschina
链接:https://my.oschina.net/lovexin/blog/3155019