Mockito 是一种 Java Mock 框架,主要是用来做 Mock 测试,它可以模拟任何 Spring 管理的 Bean、模拟方法的返回值、模拟抛出异常等等,在了解 Mockito 的具体用法之前,得先了解什么是 Mock 测试。

什么是 Mock 测试?

Mock 测试就是在测试过程中,创建一个假的对象,避免你为了测试一个方法,却要自行构建整个 Bean 的依赖链。

像是以下这张图,类 A 需要调用类 B 和类 C,而类 B 和类 C 又需要调用其他类如 D、E、F 等,假设类 D 是一个外部服务,那就会很难测,因为你的返回结果会直接的受外部服务影响,导致你的单元测试可能今天会过、但明天就过不了了。

Mockito 简介 - 图1

而当我们引入 Mock 测试时,就可以创建一个假的对象,替换掉真实的 Bean B 和 C,这样在调用B、C的方法时,实际上就会去调用这个假的 Mock 对象的方法,而我们就可以自己设定这个 Mock 对象的参数和期望结果,让我们可以专注在测试当前的类 A,而不会受到其他的外部服务影响,这样测试效率就能提高很多。

Mockito 简介 - 图2

Mockito 简介

说完了 Mock 测试的概念,接下来我们进入到今天的主题,Mockito。

Mockito 是一种 Java Mock 框架,他主要就是用来做 Mock 测试的,它可以模拟任何 Spring 管理的 Bean、模拟方法的返回值、模拟抛出异常等等,同时也会记录调用这些模拟方法的参数、调用顺序,从而可以校验出这个 Mock 对象是否有被正确的顺序调用,以及按照期望的参数被调用。

像是 Mockito 可以在单元测试中模拟一个 Service 返回的数据,而不会真正去调用该 Service,这就是上面提到的 Mock 测试精神,也就是通过模拟一个假的 Service 对象,来快速的测试当前我想要测试的类。

目前在 Java 中主流的 Mock 测试工具有 Mockito、JMock、EasyMock等等,而 SpringBoot 目前内建的是 Mockito 框架。

题外话说一下,Mockito 是命名自一种调酒莫吉托(Mojito),外国人也爱玩谐音梗……

官方

入门:5分钟了解Mockito

Mockito:一个强大的用于 Java 开发的模拟测试框架

Mockito 简明教程

在 SpringBoot 单元测试中使用 Mockito

首先在 pom.xml 下新增 spring-boot-starter-test 依赖,该依赖内就有包含了 JUnit、Mockito。

  1. < dependency>
  2. < groupId> org.springframework.boot </ groupId>
  3. < artifactId> spring-boot-starter-test </ artifactId>
  4. < scope> test </ scope>
  5. </ dependency>

先写好一个 UserService,他里面有两个方法 getUserById 和 insertUser,而他们会分别去再去调用 UserDao 这个 bean的 getUserById 和 insertUser 方法。

  1. @Component
  2. public class UserService {
  3. @Autowired
  4. private UserDao userDao;
  5. public User getUserById(Integer id){
  6. returnuserDao.getUserById(id);
  7. }
  8. publicInteger insertUser(User user){
  9. returnuserDao.insertUser(user);
  10. }
  11. }

User Model 的定义如下:

  1. public class User {
  2. privateInteger id;
  3. privateString name;
  4. //省略 getter/setter
  5. }

如果这时候我们先不使用 Mockito 模拟一个假的 userDao Bean,而是真的去调用一个正常的 Spring Bean 的 userDao 的话,测试类写法如下。其实就是很普通的注入 userService Bean,然后去调用他的方法,而他会再去调用 userDao 取得数据库的数据,然后我们再对返回结果做 Assert 断言检查。

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. public class UserServiceTest {
  4. //先普通的注入一个userService bean
  5. @Autowired
  6. private UserService userService;
  7. @Test
  8. public void getUserById() throws Exception {
  9. //普通的使用userService,他里面会再去调用userDao取得数据库的数据
  10. User user = userService.getUserById( 1);
  11. //检查结果
  12. Assert.assertNotNull(user);
  13. Assert.assertEquals(user.getId, newInteger( 1));
  14. Assert.assertEquals(user.getName, "John");
  15. }
  16. }

但是如果 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 的单元测试具体实例如下:

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. publicclass UserServiceTest {
  4. @Autowired
  5. private UserService userService;
  6. @MockBean
  7. private UserDao userDao;
  8. @Test
  9. public void getUserById() throws Exception {
  10. // 定义当调用mock userDao的getUserById方法,并且参数为3时,就返回id为200、name为I'm mock3的user对象
  11. Mockito.when(userDao.getUserById( 3)).thenReturn( newUser( 200, "I'm mock 3"));
  12. // 返回的会是名字为I'm mock 3的user对象
  13. User user = userService.getUserById( 1);
  14. Assert.assertNotNull(user);
  15. Assert.assertEquals(user.getId, newInteger( 200));
  16. Assert.assertEquals(user.getName, "I'm mock 3");
  17. }
  18. }

Mockito 除了最基本的 Mockito.when( 对象.方法名 ).thenReturn( 自定义结果 ),还提供了其他用法让我们使用。

thenReturn 系列方法

当使用任何整数值调用 userService 的 getUserById 方法时,就回传一个名字为 I’m mock3 的 User 对象。

  1. Mockito.when(userService.getUserById(Mockito.anyInt))
  2. .thenReturn( newUser( 3, "I'm mock"));
  3. User user1 = userService.getUserById( 3); // 回传的user的名字为I'm mock
  4. User user2 = userService.getUserById( 200); // 回传的user的名字也为I'm mock

限制只有当参数的数字是 3 时,才会回传名字为 I’m mock 3 的 user 对象。

  1. Mockito.when(userService.getUserById( 3)).thenReturn( newUser( 3, "I'm mock"));
  2. User user1 = userService.getUserById( 3); // 回传的user的名字为I'm mock
  3. User user2 = userService.getUserById( 200); // 回传的user为null

当调用 userService 的 insertUser 方法时,不管传进来的 user 是什么,都回传 100。

  1. Mockito.when(userService.insertUser(Mockito.any(User.class))).thenReturn( 100);
  2. Integer i = userService.insertUser( newUser); //会返回100

thenThrow 系列方法

当调用 userService 的 getUserById 时的参数是 9 时,抛出一个 RuntimeException。

  1. Mockito.when(userService.getUserById( 9))
  2. .thenThrow( new RuntimeException( "mock throw exception"));
  3. User user = userService.getUserById( 9); //会抛出一个RuntimeException

如果方法没有返回值的话(即是方法定义为 public void myMethod {…}),要改用 doThrow 抛出 Exception。

  1. Mockito.doThrow( new RuntimeException( "mock throw exception"))
  2. .when(userService).print;
  3. userService.print; //会抛出一个RuntimeException

verify 系列方法

检查调用 userService 的 getUserById、且参数为3的次数是否为1次。

  1. Mockito.verify(userService, Mockito.times( 1)).getUserById(Mockito.eq( 3)) ;

验证调用顺序,验证 userService 是否先调用 getUserById 两次,并且第一次的参数是 3、第二次的参数是 5,然后才调用insertUser 方法。

  1. InOrder inOrder = Mockito.inOrder(userService);
  2. inOrder.verify(userService).getUserById( 3);
  3. inOrder.verify(userService).getUserById( 5);
  4. inOrder.verify(userService).insertUser(Mockito.any(User.class));

Spring中mock任何容器内对象

Spring中正常使用mockito

上demo代码:

  1. @RunWith(SpringJUnit4ClassRunner.class)
  2. @ContextConfiguration(locations = { "classpath:testApplicationContext.xml" })
  3. public class SpringMockitoTest {
  4. @Mock
  5. private ApiService mockApiService;
  6. @Before
  7. public void initMocks() {
  8. MockitoAnnotations.initMocks(this);
  9. when(mockApiService.test()).thenReturn("ok");
  10. }
  11. @Test
  12. public void should_success_when_testApiService() {
  13. String result = mockApiService.test();
  14. Assert.assertEquals("ok", result);
  15. }
  16. }
  17. @Component
  18. public class ApiService {
  19. @Autowired
  20. private TestApiService testApiService;
  21. public String test() {
  22. String connect = testApiService.connect();
  23. connect += "test";//test自己的业务
  24. return connect;
  25. }
  26. }
  27. @Component
  28. public class TestApiService {
  29. public String connect() {
  30. return "error";
  31. }
  32. public String findFromDb() {
  33. return "db_data";
  34. }
  35. }

正常使用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可以把一个对象中属性设置为新值,我们可以使用:

  1. ReflectionTestUtils.setField(apiService, "testApiService", spyTestApiService);

把我们mock的testApiService放到apiService中,这样apiService调用就是我们mock的对象了;但是默认spring中apiService对象是代理对象,不能直接把值设置到属性上,所以我们自己写个小的工具类,在最后如下:

  1. ReflectionTestUtils.setField(AopTargetUtils.getTarget(apiService), "testApiService", spyTestApiService);

完整demo:

  1. @RunWith(SpringJUnit4ClassRunner.class)
  2. @ContextConfiguration(locations = { "classpath:testApplicationContext.xml" })
  3. public class SpringMockitoTest {
  4. @Autowired
  5. private ApiService apiService;
  6. @Mock
  7. private TestApiService spyTestApiService;
  8. @Autowired
  9. private TestApiService testApiService;
  10. @Before
  11. public void initMocks() throws Exception {
  12. MockitoAnnotations.initMocks(this);
  13. ReflectionTestUtils.setField(AopTargetUtils.getTarget(apiService), "testApiService", spyTestApiService);
  14. when(spyTestApiService.connect()).thenReturn("ok");
  15. }
  16. @After
  17. public void clearMocks() throws Exception {
  18. ReflectionTestUtils.setField(AopTargetUtils.getTarget(apiService), "testApiService", testApiService);
  19. }
  20. @Test
  21. public void should_success_when_testApiService() {
  22. String result = apiService.test();
  23. Assert.assertEquals("oktest", result);
  24. }
  25. }
  26. @Component
  27. public class ApiService {
  28. @Autowired
  29. private TestApiService testApiService;
  30. public String test() {
  31. String connect = testApiService.connect();
  32. connect += "test";//test自己的业务
  33. return connect;
  34. }
  35. }
  36. @Component
  37. public class TestApiService {
  38. public String connect() {
  39. return "error";
  40. }
  41. public String findFromDb() {
  42. return "db_data";
  43. }
  44. }
  45. public class AopTargetUtils {
  46. /**
  47. * 获取 目标对象
  48. * @param proxy 代理对象
  49. * @return
  50. * @throws Exception
  51. */
  52. public static Object getTarget(Object proxy) throws Exception {
  53. if(!AopUtils.isAopProxy(proxy)) {
  54. return proxy;//不是代理对象
  55. }
  56. if(AopUtils.isJdkDynamicProxy(proxy)) {
  57. return getJdkDynamicProxyTargetObject(proxy);
  58. } else { //cglib
  59. return getCglibProxyTargetObject(proxy);
  60. }
  61. }
  62. private static Object getCglibProxyTargetObject(Object proxy) throws Exception {
  63. Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
  64. h.setAccessible(true);
  65. Object dynamicAdvisedInterceptor = h.get(proxy);
  66. Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
  67. advised.setAccessible(true);
  68. Object target = ((AdvisedSupport)advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
  69. return target;
  70. }
  71. private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
  72. Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
  73. h.setAccessible(true);
  74. AopProxy aopProxy = (AopProxy) h.get(proxy);
  75. Field advised = aopProxy.getClass().getDeclaredField("advised");
  76. advised.setAccessible(true);
  77. Object target = ((AdvisedSupport)advised.get(aopProxy)).getTargetSource().getTarget();
  78. return target;
  79. }
  80. }

最后就是注意测试之后要还原现场,把spring对象还原,尤其在跑maven test的时候,否则可能会影响其他人的测试。

Mockito 的限制

上述就是 Mockito 的 Mock 对象使用方法,不过当使用 Mockito 在 Mock 对象时,有一些限制需要遵守:

  • 不能 Mock 静态方法
  • 不能 Mock private 方法
  • 不能 Mock final class

因此在写代码时,需要做良好的功能拆分,才能够使用 Mockito 的 Mock 技术,帮助我们降低测试时 Bean 的耦合度。

总结

Mockito 是一个非常强大的框架,可以在执行单元测试时帮助我们模拟一个 Bean,提高单元测试的稳定性。

并且大家可以尝试在写代码时,从 Mock 测试的角度来写,更能够写出功能切分良好的代码架构,像是如果有把专门和外部服务沟通的代码抽出来成一个 Bean,在进行单元测试时,只要透过 Mockito 更换掉那个 Bean 就行了。