Mockito:一个强大的用于 Java 开发的模拟测试框架
转自: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 就行了。