提升单元测试体验的利器--Mockito使用总结
为神马要使用Mockito?
在编写单元测试的时候,为了尽可能的保证隔离性,我们时常需要对某些不容易构造或者不容易获取或者对外部环境有依赖的对象,用一个虚拟的对象来创建以便于测试.假设你正在开发的的代码中使用到了公司其他部门的接口(通过RPC服务),当编写单元测试的时候你可能为了不让接口真的去调用rpc服务而mock一个接口的对象,最原始的方式是自己手工编写一个该接口的实现类,并且在单元测试的时候注入这个对象,而使用Mockito则可以让我们方便地创建和配置mock对象,使用mockito可以简化对外部环境的依赖.
创建mock对象
这里我们以一个操作Redis的工具类来举例,下面是代码:
public class RedisUtil { @Autowired private RedisOperations<String, String> redisTemplate; public boolean checkKeyExists(String key) { return redisTemplate.hasKey(key); } public void setValueByKey(String key, String value) { redisTemplate.opsForValue().set(key, value); }
public String getValueByKey(String key) { return redisTemplate.opsForValue().get(key); } public List<String> getMutiValuesByList(List<String> keys){ return redisTemplate.opsForValue().multiGet(keys); } public void deleteKey(String key) { redisTemplate.delete(key); } public void increValue(String key,Long count){ redisTemplate.opsForValue().increment(key,count); } }
有两种方式可以方便的创建mock对象,第一种方式是
Mockito.mock(RedisUtil.class);
还有一种方式在注入时使用@mock注解:
@mock private RedisUtil redisUtil;
Tips:如果在代码中频繁的使用Mockito比较烦,可以静态导入package,以下例子全部默认已静态导入Mockito包:
import static org.mockito.Mockito.*;
为测试函数打桩的常用方法
当对象被创建之后,就可以对代码中出现的方法进行自定义的交互,mock对象会记住这些交互,在单元测试的执行中碰到代码中的对应方法会默认执行被你自定义的方法内容.
还是以RedisUtil为例,对方法设定返回值:
when(redisUtil.getValueByKey("key1")).thenReturn("value1");
when(redisUtil.getValueByKey("key2")).thenReturn("value2");
对方法设定返回自定义的异常信息:
when(redisUtil.getValueByKey("key1")).thenThrow(new RuntimeException);
此外Mockito还支持迭代风格的返回值定义:
when(redisUtil.getValueByKey("key1")).thenReturn("value1").thenReturn("Value2");
即当方法第一次调用redis.getValueBykey("key1")时会返回value1,当再次被调用时则会返回value2.这里需要注意的是,当后续再出现调用的时候返回值都会是value2,而且这种迭代风格的定义支持return和Throw的混搭,即你可以控制在函数调用的第一次去抛出一个异常,而在函数调用的第二次绘制一个正常的值.
Mockito如何mock返回值为void的方法
首先,测试中对于返回值为void 的方法进行mock本身是没有什么效果的,Mockito有一个doNothing方法是void方法的默认返回:
doNothing().when(redisUtil).increValue(“key1",1L);
其实这里使用doNothing来mock这个方法并没有什么意义,因为我们mock一个方法的目的无非有两个,第一,在某一中输入环境中模拟返回我们期待的返回值,第二就是当方法抛出异常时能够在我们预期控制之下而不会导致单元测试失败,因此对于返回值为void的方法,我们一般可以不去mock它或者使用doThrow()来为void函数打一个桩,当出现异常的时候mock他的异常返回,当不会有异常发生时,只需要在调用后,verify()一下,验证方法的被调用次数即可.
verify(redisUtil,times(1)).increValue("key1",1L);
代码中的times(1)表示一次,即代码中increValue()返回被调用一次的时候能够通过,还可以支持更加广泛的定义,
never():表示从未被调用
atleastOnce():表示至少被调用一次
atleast(3):表示至少被调用3次
atMost(7):表示最多被调用7次
参数匹配器
这里主要介绍一下内置的几个参数匹配器,其实也很好理解,还是那上面的redisUtil为例,对于redisUtil.getValueByKey来说,我希望对于任意的key都返回同一个值,那就可以这么写:
when(redisUtil.getValueByKey(anyString()).thenReturn("value1")
这样在单元测试过程中,对于任意的输入参数,该方法都会返回value1,相同的类型还有很多anyLong(),anyInt(),anyList()等等
使用Spy对象来监控真实对象
以上所讲的对象都是mock对象,mock对象只能调用打桩方法,不能调用真实方法,使用Spy可以让我们能够监视一个真实对象,既可以对这个对象的某一个函数打桩返回我们期望的值,也可以去调用真实的方法,创建spy对象的方式和mock类似,不同的一点是spy需要传一个真实对象而不是一个CLass对象.这里以一个List为例,
List spy = spy(new LinkedList()); when(spy.get(0)).thenReturn("value1"); doReturn("value2").when(spy).get(0);
上面第二行代码,调用when(spy.get(0)),会去调用真实的方法,会抛出异常,第三行代码则不会去调用真实方法,而返回value2.所以总结一下就是,当使用when去模拟返回值的时候,真是方法会被调用,而是用doReturn()去设置的话,则不会去执行真实方法.
需要注意在使用时应该尽量避免使用spy.
参考文档