Mockito 简介
Mockito 是一种 Java Mock 框架,主要是用来做 Mock 测试,它可以模拟任何 Spring 管理的 Bean、模拟方法的返回值、模拟抛出异常等等,在了解 Mockito 的具体用法之前,得先了解什么是 Mock 测试。
什么是 Mock 测试?
Mock 测试就是在测试过程中,创建一个假的对象,避免你为了测试一个方法,却要自行构建整个 Bean 的依赖链。
像是以下这张图,类 A 需要调用类 B 和类 C,而类 B 和类 C 又需要调用其他类如 D、E、F 等,假设类 D 是一个外部服务,那就会很难测,因为你的返回结果会直接的受外部服务影响,导致你的单元测试可能今天会过、但明天就过不了了。
而当我们引入 Mock 测试时,就可以创建一个假的对象,替换掉真实的 Bean B 和 C,这样在调用B、C的方法时,实际上就会去调用这个假的 Mock 对象的方法,而我们就可以自己设定这个 Mock 对象的参数和期望结果,让我们可以专注在测试当前的类 A,而不会受到其他的外部服务影响,这样测试效率就能提高很多。
Mockito 简介
说完了 Mock 测试的概念,接下来我们进入到今天的主题,Mockito。
Mockito 是一种 Java Mock 框架,他主要就是用来做 Mock 测试的,它可以模拟任何 Spring 管理的 Bean、模拟方法的返回值、模拟抛出异常等等,同时也会记录调用这些模拟方法的参数、调用顺序,从而可以校验出这个 Mock 对象是否有被正确的顺序调用,以及按照期望的参数被调用。
像是 Mockito 可以在单元测试中模拟一个 Service 返回的数据,而不会真正去调用该 Service,这就是上面提到的 Mock 测试精神,也就是通过模拟一个假的 Service 对象,来快速的测试当前我想要测试的类。
目前在 Java 中主流的 Mock 测试工具有 Mockito、JMock、EasyMock等等,而 SpringBoot 目前内建的是 Mockito 框架。
题外话说一下,Mockito 是命名自一种调酒莫吉托(Mojito),外国人也爱玩谐音梗……
在 SpringBoot 单元测试中使用 Mockito
首先在 pom.xml 下新增 spring-boot-starter-test 依赖,该依赖内就有包含了 JUnit、Mockito。
< dependency>
< groupId> org.springframework.boot </ groupId>
< artifactId> spring-boot-starter-test </ artifactId>
< scope> test </ scope>
</ dependency>
先写好一个 UserService,他里面有两个方法 getUserById 和 insertUser,而他们会分别去再去调用 UserDao 这个 bean的 getUserById 和 insertUser 方法。
@Component
public class UserService {
@Autowired
private UserDao userDao;
public User getUserById(Integer id){
returnuserDao.getUserById(id);
}
publicInteger insertUser(User user){
returnuserDao.insertUser(user);
}
}
User Model 的定义如下:
public class User {
privateInteger id;
privateString name;
//省略 getter/setter
}
如果这时候我们先不使用 Mockito 模拟一个假的 userDao Bean,而是真的去调用一个正常的 Spring Bean 的 userDao 的话,测试类写法如下。其实就是很普通的注入 userService Bean,然后去调用他的方法,而他会再去调用 userDao 取得数据库的数据,然后我们再对返回结果做 Assert 断言检查。
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {
//先普通的注入一个userService bean
@Autowired
private UserService userService;
@Test
public void getUserById() throws Exception {
//普通的使用userService,他里面会再去调用userDao取得数据库的数据
User user = userService.getUserById( 1);
//检查结果
Assert.assertNotNull(user);
Assert.assertEquals(user.getId, newInteger( 1));
Assert.assertEquals(user.getName, "John");
}
}
但是如果 userDao 还没写好,又想先测 userService 的话,就需要使用 Mockito 去模拟一个假的 userDao 出来。
使用方法是在 userDao 上加上一个 @MockBean 注解,当 userDao 被加上这个注解之后,表示 Mockito 会帮我们创建一个假的 Mock 对象,替换掉 Spring 中已存在的那个真实的 userDao Bean,也就是说,注入进 userService 的 userDao Bean,已经被我们替换成假的 Mock 对象了,所以当我们再次调用 userService 的方法时,会去调用的实际上是 mock userDao Bean 的方法,而不是真实的 userDao Bean。
当我们创建了一个假的 userDao 后,我们需要为这个 mock userDao 自定义方法的返回值,这里有一个公式用法,下面这段代码的意思为,当调用了某个 Mock 对象的方法时,就回传我们想要的自定义结果。
Mockito.when( 对象.方法名 ).thenReturn( 自定义结果 )
使用 Mockito 模拟 Bean 的单元测试具体实例如下:
@RunWith(SpringRunner.class)
@SpringBootTest
publicclass UserServiceTest {
@Autowired
private UserService userService;
@MockBean
private UserDao userDao;
@Test
public void getUserById() throws Exception {
// 定义当调用mock userDao的getUserById方法,并且参数为3时,就返回id为200、name为I'm mock3的user对象
Mockito.when(userDao.getUserById( 3)).thenReturn( newUser( 200, "I'm mock 3"));
// 返回的会是名字为I'm mock 3的user对象
User user = userService.getUserById( 1);
Assert.assertNotNull(user);
Assert.assertEquals(user.getId, newInteger( 200));
Assert.assertEquals(user.getName, "I'm mock 3");
}
}
Mockito 除了最基本的 Mockito.when( 对象.方法名 ).thenReturn( 自定义结果 ),还提供了其他用法让我们使用。
thenReturn 系列方法
当使用任何整数值调用 userService 的 getUserById 方法时,就回传一个名字为 I'm mock3 的 User 对象。
Mockito.when(userService.getUserById(Mockito.anyInt)).thenReturn( newUser( 3, "I'm mock"));
User user1 = userService.getUserById( 3); // 回传的user的名字为I'm mock
User user2 = userService.getUserById( 200); // 回传的user的名字也为I'm mock
限制只有当参数的数字是 3 时,才会回传名字为 I'm mock 3 的 user 对象。
Mockito.when(userService.getUserById( 3)).thenReturn( newUser( 3, "I'm mock"));
User user1 = userService.getUserById( 3); // 回传的user的名字为I'm mock
User user2 = userService.getUserById( 200); // 回传的user为null
当调用 userService 的 insertUser 方法时,不管传进来的 user 是什么,都回传 100。
Mockito.when(userService.insertUser(Mockito.any(User.class))).thenReturn( 100);
Integer i = userService.insertUser( newUser); //会返回100
thenThrow 系列方法
当调用 userService 的 getUserById 时的参数是 9 时,抛出一个 RuntimeException。
Mockito.when(userService.getUserById( 9)).thenThrow( new RuntimeException( "mock throw exception"));
User user = userService.getUserById( 9); //会抛出一个RuntimeException
如果方法没有返回值的话(即是方法定义为 public void myMethod {...}),要改用 doThrow
抛出 Exception。
Mockito.doThrow( new RuntimeException( "mock throw exception")).when(userService).print;
userService.print; //会抛出一个RuntimeException
verify 系列方法
检查调用 userService 的 getUserById、且参数为3的次数是否为1次。
Mockito.verify(userService, Mockito.times( 1)).getUserById(Mockito.eq( 3)) ;
验证调用顺序,验证 userService 是否先调用 getUserById 两次,并且第一次的参数是 3、第二次的参数是 5,然后才调用insertUser 方法。
InOrder inOrder = Mockito.inOrder(userService);
inOrder.verify(userService).getUserById( 3);
inOrder.verify(userService).getUserById( 5);
inOrder.verify(userService).insertUser(Mockito.any(User.class));
Spring中mock任何容器内对象
Spring中正常使用mockito
上demo代码:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:testApplicationContext.xml" })
public class SpringMockitoTest {
@Mock
private ApiService mockApiService;
@Before
public void initMocks() {
MockitoAnnotations.initMocks(this);
when(mockApiService.test()).thenReturn("ok");
}
@Test
public void should_success_when_testApiService() {
String result = mockApiService.test();
Assert.assertEquals("ok", result);
}
}
@Component
public class ApiService {
@Autowired
private TestApiService testApiService;
public String test() {
String connect = testApiService.connect();
connect += "test";//test自己的业务
return connect;
}
}
@Component
public class TestApiService {
public String connect() {
return "error";
}
public String findFromDb() {
return "db_data";
}
}
正常使用spring和mockito中,我们把需要的mock的ApiService
给mock掉,但是我们更想的是把TestApiService
中的connect方法mock掉,这样就可以测试我们自己的代码,也就是ApiService中test方法自己的业务。
Spring中mock任何容器内对象
上面的demo中,我们如何mock掉TestApiService
中的test方法?
因为TestApiService
是spring容器管理的bean,并且ApiService
中使用到TestApiService
,所以我们把ApiService
中引用的TestApiService
替换成我们的mock对象即可。
Spring框架有个反射工具ReflectionTestUtils
,可以把一个对象中属性设置为新值,我们可以使用:
ReflectionTestUtils.setField(apiService, "testApiService", spyTestApiService);
把我们mock的testApiService
放到apiService
中,这样apiService
调用就是我们mock的对象了;但是默认spring中apiService
对象是代理对象,不能直接把值设置到属性上,所以我们自己写个小的工具类,在最后如下:
ReflectionTestUtils.setField(AopTargetUtils.getTarget(apiService), "testApiService", spyTestApiService);
完整demo:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:testApplicationContext.xml" })
public class SpringMockitoTest {
@Autowired
private ApiService apiService;
@Mock
private TestApiService spyTestApiService;
@Autowired
private TestApiService testApiService;
@Before
public void initMocks() throws Exception {
MockitoAnnotations.initMocks(this);
ReflectionTestUtils.setField(AopTargetUtils.getTarget(apiService), "testApiService", spyTestApiService);
when(spyTestApiService.connect()).thenReturn("ok");
}
@After
public void clearMocks() throws Exception {
ReflectionTestUtils.setField(AopTargetUtils.getTarget(apiService), "testApiService", testApiService);
}
@Test
public void should_success_when_testApiService() {
String result = apiService.test();
Assert.assertEquals("oktest", result);
}
}
@Component
public class ApiService {
@Autowired
private TestApiService testApiService;
public String test() {
String connect = testApiService.connect();
connect += "test";//test自己的业务
return connect;
}
}
@Component
public class TestApiService {
public String connect() {
return "error";
}
public String findFromDb() {
return "db_data";
}
}
public class AopTargetUtils {
/**
* 获取 目标对象
* @param proxy 代理对象
* @return
* @throws Exception
*/
public static Object getTarget(Object proxy) throws Exception {
if(!AopUtils.isAopProxy(proxy)) {
return proxy;//不是代理对象
}
if(AopUtils.isJdkDynamicProxy(proxy)) {
return getJdkDynamicProxyTargetObject(proxy);
} else { //cglib
return getCglibProxyTargetObject(proxy);
}
}
private static Object getCglibProxyTargetObject(Object proxy) throws Exception {
Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
h.setAccessible(true);
Object dynamicAdvisedInterceptor = h.get(proxy);
Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
advised.setAccessible(true);
Object target = ((AdvisedSupport)advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
return target;
}
private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
h.setAccessible(true);
AopProxy aopProxy = (AopProxy) h.get(proxy);
Field advised = aopProxy.getClass().getDeclaredField("advised");
advised.setAccessible(true);
Object target = ((AdvisedSupport)advised.get(aopProxy)).getTargetSource().getTarget();
return target;
}
}
最后就是注意测试之后要还原现场,把spring对象还原,尤其在跑maven test的时候,否则可能会影响其他人的测试。
Mockito 的限制
上述就是 Mockito 的 Mock 对象使用方法,不过当使用 Mockito 在 Mock 对象时,有一些限制需要遵守:
- 不能 Mock 静态方法
- 不能 Mock private 方法
- 不能 Mock final class
因此在写代码时,需要做良好的功能拆分,才能够使用 Mockito 的 Mock 技术,帮助我们降低测试时 Bean 的耦合度。
总结
Mockito 是一个非常强大的框架,可以在执行单元测试时帮助我们模拟一个 Bean,提高单元测试的稳定性。
并且大家可以尝试在写代码时,从 Mock 测试的角度来写,更能够写出功能切分良好的代码架构,像是如果有把专门和外部服务沟通的代码抽出来成一个 Bean,在进行单元测试时,只要透过 Mockito 更换掉那个 Bean 就行了。
Mockito 简介的更多相关文章
- Mockito简介(转)
Mockito 是目前 java 单测中使用比较流行的 mock 工具.其他还有 EasyMock,JMock,MockCreator,Mockrunner,MockMaker 及 PowerMock ...
- 单元测试系列:Mock工具之Mockito实战
更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6780719.html 在实际项目中写单 ...
- Mockito单元测试
Mockito简介 Mockito是一个单元测试框架,需要Junit的支持.在我们的项目中,都存在相当多的依赖关系,当我们在测试某一个业务相关的接口或则方法时,绝大多数时候是没有办法或则很难去添加所有 ...
- 单元测试系列之五:Mock工具之Mockito实战
更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6780719.html 在实际项目中写单 ...
- Java测试框架Mockito源码分析
1.Mockito简介 测试驱动的开发(Test Driven Design, TDD)要求我们先写单元测试,再写实现代码.在写单元测试的过程中,一个很普遍的问题是,要测试的类会有很多依赖,这些依赖的 ...
- Mockito 库、powermock扩展
转载:http://blog.csdn.net/kittyboy0001/article/details/18709685 Mockito 简介 Mockito 是目前 java 单测中使用比较流行的 ...
- 单元测试系列:Mock工具Jmockit使用介绍
更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6760272.html Mock工具Jm ...
- 单元测试系列之二:Mock工具Jmockit实战
更多原创测试技术文章同步更新到微信公众号 :三国测,敬请扫码关注个人的微信号,感谢! 原文链接:http://www.cnblogs.com/zishi/p/6760272.html Mock工具Jm ...
- spring /spring boot中mock
1 Mockito简介 1.1 Mockito是什么 Mockito是一个简单的流行的Mock框架.它允许你创建和配置mock对象.使用Mockito可以明显的简化对外部依赖的测试类的开发.一般使 ...
随机推荐
- python -m参数
把模块当做脚本运行,标准库和第三方库都可以 会把当前路径添加到sys.path中
- Spring Cloud Gateway实战之三:动态路由
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- Windows11 如何让开始菜单出现休眠选项(Windows11如何开启休眠功能?)Windows家庭版
1. Windows11新版的菜单找不到调休眠的选项了,所以我们可以用win+r键调出运行,输入control,回车调出「控制面板」 配图: 2. 我的电脑系统是家庭版的Windows11,其它版本这 ...
- [bzoj5340]假面
修改:维护g[i][j]表示第i个数为j的概率,从前往后转移 转移方程:g[id][i]=g[id][i-1]*p+g[id][i]*(1-p),初始g[i][a[i]]=1 询问:对于每一个人i,输 ...
- Insights直播回顾,对话专家,HMS Core 6创新能力解读
HMS Core Insights第八期直播–对话专家,HMS Core 6创新能力解读,已于11月25日圆满结束,本期直播与小伙伴们一同了解了HMS Core 6在图形.媒体以及连接与通信领域推出的 ...
- linux命令-压缩数据
linux文件压缩工具:bzip2 文件扩展名 .bz2 compress 文件扩展名 .Z linux上很少看到了 uncompress解压 gzip 文件扩展名,.gz,gzip压缩文件,gzca ...
- setoolkit的钓鱼实验
1.在kali中打开setoolkit 2.在菜单中选择第一个进入社会工程学攻击 3.选择第二个模块属于网站攻击向量 4.选择第五个模块,进行web劫持攻击 5.选择第二个,进行网站克隆 6.发现访问 ...
- [NOIP2017 提高组] 列队
考虑我们需要维护的是这样一个东西. 即可能变化的只有每一行前\(m - 1\)个,和最后一列. 我们考虑对每一行开一个权值线段树,记录原本序列的第\(x\)个是否被一出,且用一个\(vector\)记 ...
- R包MetaboAnalystR安装指南(Linux环境非root)
前言 这是代谢组学数据分析的一个R包,包括用于代谢组学数据分析.可视化和功能注释等众多功能.最近有同事在集群中搭建蛋白和代谢流程,安装这个包出现了问题,于是我折腾了一上午. 这个包的介绍在:https ...
- 3D-DNA 挂载染色体
3D-DNA是一款简单,方便的处理Hi-C软件,可将contig提升到染色体水平.其githup网址:https://github.com/theaidenlab/3d-dna 3D-DNA流程简介 ...