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
⾸先在 l 下新增 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){
}
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 = UserById( 1);
//检查结果
Assert.assertNotNull(user);
Assert.Id, newInteger( 1));
Assert.Name, "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 的单元测试具体实例如下:springboot实现aop
@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.UserById( 3)).thenReturn( newUser( 200, "I'm mock 3"));
// 返回的会是名字为I'm mock 3的user对象
User user = UserById( 1);
Assert.assertNotNull(user);
Assert.Id, newInteger( 200));
Assert.Name, "I'm mock 3");
}
}
Mockito 除了最基本的 Mockito.when( 对象.⽅法名 ).thenReturn( ⾃定义结果 ),还提供了其他⽤法让我们使⽤。
thenReturn 系列⽅法
当使⽤任何整数值调⽤ userService 的 getUserById ⽅法时,就回传⼀个名字为 I'm mock3 的 User 对象。
Mockito.UserById(Mockito.anyInt)).thenReturn( newUser( 3, "I'm mock"));
User user1 = UserById( 3); // 回传的user的名字为I'm mock
User user2 = UserById( 200); // 回传的user的名字也为I'm mock
限制只有当参数的数字是 3 时,才会回传名字为 I'm mock 3 的 user 对象。
Mockito.UserById( 3)).thenReturn( newUser( 3, "I'm mock"));
User user1 = UserById( 3); // 回传的user的名字为I'm mock
User user2 = UserById( 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.UserById( 9)).thenThrow( new RuntimeException( "mock throw exception"));
User user = UserById( 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 = { "l" })
public class SpringMockitoTest {
@Mock
private ApiService mockApiService;
@Before
public void initMocks() {
MockitoAnnotations.initMocks(this);
st()).thenReturn("ok");
}
@Test
public void should_success_when_testApiService() {
String result = st();
Assert.assertEquals("ok", result);
}
}
@Component
public class ApiService {
@Autowired
private TestApiService testApiService;
public String test() {
String connect = t();
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,所以我们把Api
Service中引⽤的T estApiService替换成我们的mock对象即可。
Spring框架有个反射⼯具ReflectionTestUtils,可以把⼀个对象中属性设置为新值,我们可以使⽤:
ReflectionTestUtils.setField(apiService, "testApiService", spyTestApiService);
把我们mock的testApiService放到apiService中,这样apiService调⽤就是我们mock的对象了;但是默认spring中apiService对象是代理对象,不能直接把值设置到属性上,所以我们⾃⼰写个⼩的⼯具类,在最后如下:
ReflectionTestUtils.Target(apiService), "testApiService", spyTestApiService);
完整demo:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "l" })
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.Target(apiService), "testApiService", spyTestApiService);
t()).thenReturn("ok");
}
@After
public void clearMocks() throws Exception {
ReflectionTestUtils.Target(apiService), "testApiService", testApiService);
}
@Test
public void should_success_when_testApiService() {
String result = st();
Assert.assertEquals("oktest", result);
}
}
@Component
public class ApiService {
@Autowired
private TestApiService testApiService;
public String test() {
String connect = t();
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 = Class().getDeclaredField("CGLIB$CALLBACK_0");
h.setAccessible(true);
Object dynamicAdvisedInterceptor = h.get(proxy);
Field advised = Class().getDeclaredField("advised");
advised.setAccessible(true);
Object target = (((dynamicAdvisedInterceptor)).getTargetSource().getTarget();
return target;
}
private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
Field h = Class().getSuperclass().getDeclaredField("h");
h.setAccessible(true);
AopProxy aopProxy = (AopProxy) h.get(proxy);
Field advised = Class().getDeclaredField("advised");
advised.setAccessible(true);
Object target = (((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 就⾏了。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论