封面来源:本文封面来源于网络,如有侵权,请联系删除。
本文参考:汪文君Mockito实战视频
Mockito 官网:Mockito
Mockito 英文文档:Mockito-Doc-EN
Mockito 中文文档:Mockito-Doc-ZH
1. Mockito
1.1 什么是 Mock 测试
参考链接:
Mockito 简明教程
Mockito:测试框架基础使用
Mock 一词本意是指模仿或者效仿。 因此可以将 Mock 理解为替身,替代者。在软件开发中的 Mock,通常理解为模拟对象或者 Fake。
Mock 测试就是在测试过程中,对于某些不容易构造(如 HttpServletRequest 必须在 Servlet 容器中才能构造出来)或者不容易获取比较复杂的对象(如 JDBC 中的 ResultSet 对象),可以用一个虚拟的对象(Mock 对象)来创建以便测试的测试方法。
Mock 最大的功能是帮你把单元测试的耦合分解开,如果你的代码对另一个类或者接口有依赖,它能够帮你模拟这些依赖,并帮你验证所调用的依赖的行为。
那 Mock 有什么好处呢?
提前创建测试,TDD(测试驱动开发):当 Service 接口创建后就可以写单元测试了,而不用依赖实现类
团队并行工作
可以创建一个验证或演示程序
隔离系统
Mock 对象使用范畴:
真实对象具有不可确定的行为,产生不可预测的效果(如:股票行情,天气预报)时就可以使用 Mock:
真实对象很难被创建的
真实对象的某些行为很难被触发
真实对象实际上还不存在的(和其他开发小组或者和新的硬件打交道)等等
使用 Mock 对象测试的关键步骤:
使用一个接口来描述这个对象
在产品代码中实现这个接口
在测试代码中实现这个接口
在被测试代码中只是通过接口来引用对象,所以它不知道这个引用的对象是真实对象,还是 Mock 对象。
1.2 Mockito
Tasty mocking framework for unit tests in Java
参考链接:手把手教你 Mockito 的使用
什么是 Mockito?
Mockito 是一个强大并用于 Java 开发的模拟测试框架,通过 Mockito 我们可以创建和配置 Mock 对象,进而简化有外部依赖的类的测试。
使用 Mockito 的大致流程如下:
为什么要使用 Mockito?
假设现在有一个 UserService,它依赖于 UserDao、DB、AuthService,那当我们要测试 UserService 时,首先能想到的就是构建真实的 UserDao、DB、AuthService 实例并注入到 UserService 中。
那如果有一个 Service 依赖了很多个呢?
构建每个依赖的实例,然后注入进去?
这是一种笨重而繁琐的方法,这时候就可以 Mock Object 然后进行测试,而 Mock Object 可以借助 Mockito 来实现。
2. Quick Start
2.1 环境准备
导入相关依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <dependency > <groupId > org.mockito</groupId > <artifactId > mockito-all</artifactId > <version > 1.10.19</version > <scope > test</scope > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.12</version > <scope > test</scope > </dependency > <dependency > <groupId > javax.servlet</groupId > <artifactId > javax.servlet-api</artifactId > <version > 3.1.0</version > <scope > provided</scope > </dependency >
本文不使用任何 Spring 家族的框架,Demo 级别的代码对象的创建使用 new
就行,没啥耦不耦合的。
注意: 不建议直接导入 mockito-all 依赖,这样导入可能会出现一些错误,我也是在后续才发现这个问题。建议导入 mockito-core 核心包:
1 2 3 4 5 6 7 <dependency > <groupId > org.mockito</groupId > <artifactId > mockito-core</artifactId > <version > xxxx</version > <scope > test</scope > </dependency >
项目结构与代码准备
创建一个没有任何属性的实体类:
1 2 3 4 5 public class Account {}
编写 Dao 层的代码,假设 DB 不存在,直接抛出异常:
1 2 3 4 5 6 7 8 9 10 public class AccountDao { public Account findAccount (String username, String password) { throw new UnsupportedOperationException (); } }
编写 Controller 层的代码,模拟用户登录:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class AccountLoginController { private final AccountDao accountDao; public AccountLoginController (AccountDao accountDao) { this .accountDao = accountDao; } public String login (HttpServletRequest request) { final String username = request.getParameter("username" ); final String password = request.getParameter("password" ); try { Account account = accountDao.findAccount(username, password); if (account == null ) { return "/login" ; } else { return "/index" ; } } catch (Exception e) { return "/505" ; } } }
2.2 测试代码
创建一个名为 AccountLoginControllerTest
的测试类,对 AccountLoginController
中的 login()
方法进行测试,测试共有三种情况:
1、未找到用户
2、成功找到用户
3、抛出异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 @RunWith(MockitoJUnitRunner.class) public class AccountLoginControllerTest { private AccountDao accountDao; private HttpServletRequest request; private AccountLoginController accountLoginController; @Before public void before () { this .accountDao = Mockito.mock(AccountDao.class); this .request = Mockito.mock(HttpServletRequest.class); this .accountLoginController = new AccountLoginController (accountDao); } @Test public void testLoginSuccess () { Account account = new Account (); Mockito.when(request.getParameter("username" )).thenReturn("mofan" ); Mockito.when(request.getParameter("password" )).thenReturn("123456" ); Mockito.when(accountDao.findAccount(Matchers.anyString(), Matchers.anyString())).thenReturn(account); Assert.assertEquals(accountLoginController.login(request), "/index" ); } @Test public void testLoginFailure () { Mockito.when(request.getParameter("username" )).thenReturn("默烦" ); Mockito.when(request.getParameter("password" )).thenReturn("147258" ); Mockito.when(accountDao.findAccount(Matchers.anyString(), Matchers.anyString())).thenReturn(null ); Assert.assertEquals(accountLoginController.login(request), "/login" ); } @Test @SuppressWarnings("unchecked") public void testLogin505 () { Mockito.when(request.getParameter("username" )).thenReturn("404" ); Mockito.when(request.getParameter("password" )).thenReturn("500" ); Mockito.when(accountDao.findAccount(Matchers.anyString(), Matchers.anyString())) .thenThrow(UnsupportedOperationException.class); Assert.assertEquals(accountLoginController.login(request), "/505" ); } }
运行上述测试方法,无论是单个运行,还是组合运行都能够测试通过。
3. How to mock
3.1 几种不同的 Mock 方式
@RunWith(MockitoJUnitRunner.class)
使用这种方式,最重要的就是需要在测试类上使用 @RunWith(MockitoJUnitRunner.class)
注解。
之后,在测试方法中可以使用 Mockito.mock()
方法来 mock 我们需要的对象。
Mockito.mock()
方法有多个重载,比如:
public static <T> T mock(Class<T> classToMock)
public static <T> T mock(Class<T> classToMock, Answer defaultAnswer)
等等
如果我们不指定 mock()
方法的 Answer 参数,那么使用 mock 出来的对象调用方法会返回 null。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import org.junit.runner.RunWith;import org.mockito.runners.MockitoJUnitRunner;@RunWith(MockitoJUnitRunner.class) public class MockByRunnerTest { @Test public void testMock () { AccountDao accountDao = Mockito.mock(AccountDao.class, Mockito.RETURNS_SMART_NULLS); Account account = accountDao.findAccount("x" , "x" ); System.out.println(account); } }
运行测试代码后,控制台输出:
SmartNull returned by this unstubbed method call on a mock:
accountDao.findAccount("x", "x");
备注: @RunWith(MockitoJUnitRunner.class)
是为单元测试提供 mock 初始化工作。使用静态方法 Mockito.mock()
可以不用进行初始化,因为 这里的代码不使用 @RunWith
注解也是可以测试通过的 。控制台输出的结果与上述结果一致。
@Mock 注解
使用这种方式,需要在测试之前执行 MockitoAnnotations.initMocks(this);
,然后在测试类中声明一个成员变量(这个成员变量就是我们 mock 的对象),并在这个成员变量上使用 @Mock
注解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class MockByAnnotationTest { @Mock private AccountDao accountDao; @Before public void init () { MockitoAnnotations.initMocks(this ); } @Test public void testMock () { Account account = accountDao.findAccount("x" , "x" ); System.out.println(account); } }
执行这个测试方法会输出 null
,就相当于 Mockito.mock()
方法没有指定 Answer 类型的参数,那如果想要输出和第一种一样的结果该怎么办?
可以指定 @Mock
注解的 answer 属性值:
1 2 @Mock(answer = Answers.RETURNS_SMART_NULLS) private AccountDao accountDao;
使用 @Mock
注解时,必须为单元测试提供了 mock 初始化工作后才能使用。初始化方式有两种:
@RunWith(MockitoJUnitRunner.class)
MockitoAnnotations.initMocks(this);
因此,下述代码也是可以测试通过并输出与上述代码一样的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 @RunWith(MockitoJUnitRunner.class) public class MockByAnnotationTest { @Mock private AccountDao accountDao; @Test public void testMock () { Account account = accountDao.findAccount("x" , "x" ); System.out.println(account); } }
通过 Rule
使用这种方式,需要在测试类中添加:
1 2 @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
然后 mock 需要的对象就可以了。比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import org.junit.Rule;import org.junit.Test;import org.mockito.Mockito;import org.mockito.junit.MockitoJUnit;import org.mockito.junit.MockitoRule;public class MockByRuleTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Test public void testMock () { AccountDao accountDao = Mockito.mock(AccountDao.class); Account account = accountDao.findAccount("x" , "x" ); System.out.println(account); } }
也可以使用注解 @Mock
,该注解的作用和 Mockito.mock()
是一样的,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class MockByRuleTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock AccountDao accountDao; @Test public void testMock () { Account account = accountDao.findAccount("x" , "x" ); System.out.println(account); } }
除此之外,还可以使用 spy 的方式来 mock 对象,先买个关子,咱们后续说。
@RunWith(MockitoJUnitRunner.class) 与 MockitoAnnotations.initMocks(this);
作用一:
@RunWith(MockitoJUnitRunner.class)
和 MockitoAnnotations.initMocks(this);
都可以为单元测试提供框架使用的自动验证。
在编写单元测试时,若在 mock 数据有语法或者书写错误,框架使用的自动验证会在单元测试运行的时候报告出来。比如:
使用 Mockito.when()
方法时,后面没有跟 thenReturn()
、thenThrow()
等方法
使用 doReturn()
后再使用 when()
时,后面没有跟方法的调用
作用二:
@RunWith(MockitoJUnitRunner.class)
和 MockitoAnnotations.initMocks(this);
都可以为单元测试提供 mock 初始化工作。
当我们使用了 @Mock
,@Spy
, @InjectMocks
等注解时,必须进行初始化才能使用。
若在单元测试类中使用了@RunWith(SpringJUnit4ClassRunner.class)
就不能再使用@RunWith(MockitoJUnitRunner.class)
,可以使用 MockitoAnnotations.initMocks(this)
来代替。
3.2 DeepMock
环境准备
创建一个没有任何属性的 User 实体类:
1 2 3 4 5 6 public class User { public void foo () { throw new RuntimeException (); } }
创建一个 UserService 类,这个类中有一个 get()
方法,这个方法的返回值类型是 User:
1 2 3 4 5 6 7 8 9 public class UserService { public User get () { throw new RuntimeException (); } }
测试类的编写
假设需要在这里测试一下 UserService 的 get()
方法并调用 foo()
,根据前面讲的,很容易想到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class DeepMockTest { @Mock private UserService userService; @Before public void init () { MockitoAnnotations.initMocks(this ); } @Test public void testDeepMock () { User user = userService.get(); user.foo(); } }
但是这样做很明显有问题,因为 userService
是我们 mock 出来的,这个对象调用 get()
方法会返回 null
,然后再调用 foo()
方法必定出现空指针异常。
既然如此,可以在 mock 一个 User
对象,并使 get()
方法返回的是 mock 出来的 User
对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class DeepMockTest { @Mock private UserService userService; @Mock private User user; @Before public void init () { MockitoAnnotations.initMocks(this ); } @Test public void testDeepMock () { Mockito.when(userService.get()).thenReturn(user); User user = userService.get(); user.foo(); } }
向上述这样写,就不会出现空指针异常,因为我们指定了执行的行为,get()
方法返回的是 mock 出来的 User
对象。
但是这样也有一个小问题,假设我们的方法调用有很多层,那岂不是得 mock 每层的返回值,这就很不优雅。有没有一种方式可以实现只 mock 最顶层的,然后后续的返回值会自动帮我们 mock 呢?
这种方式成为 Deep Mock, 实现方式很简单,指定 @Mock
注解的 answer 属性值为 RETURNS_DEEP_STUBS
即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class DeepMockTest { @Mock(answer = Answers.RETURNS_DEEP_STUBS) private UserService userService; @Before public void init () { MockitoAnnotations.initMocks(this ); } @Test public void testDeepMock () { User user = userService.get(); user.foo(); } }
4. Mockito Stubbing
4.1 怎么使用 Stubbing
在这节,主要讲述 Stubbing 的基本使用,以 ArrayList<String>
为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @RunWith(MockitoJUnitRunner.class) public class StubbingTest { private ArrayList<String> list; @Before @SuppressWarnings("unchecked") public void init () { this .list = Mockito.mock(ArrayList.class); } @After @SuppressWarnings("unchecked") public void destroy () { Mockito.reset(list); } @Test public void howToUseStubbing () { Mockito.when(list.get(0 )).thenReturn("first" ); Assert.assertThat(list.get(0 ), CoreMatchers.equalTo("first" )); Mockito.when(list.get(Mockito.anyInt())).thenThrow(new RuntimeException ()); try { String s = list.get(0 ); Assert.fail(); } catch (Exception e) { Assert.assertThat(e, CoreMatchers.instanceOf(RuntimeException.class)); } } }
4.2 void 类型的方法
但是并不是所有方法都有返回值,还有些方法的返回值类型是 void
,那么返回值是 void
类型的方法怎么使用 Stubbing 呢?
在这节,主要讲述返回值类型是 void
的方法,然后进行以下测试:
测试方法在 4.1 StubbingTest
类中编写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void howToStubbingVoidMethod () { Mockito.doNothing().when(list).clear(); list.clear(); Mockito.verify(list, Mockito.times(1 )).clear(); Mockito.doThrow(RuntimeException.class).when(list).clear(); try { list.clear(); Assert.fail(); } catch (Exception e) { Assert.assertThat(e, CoreMatchers.instanceOf(RuntimeException.class)); } }
4.3 doReturn
前面的示例中,我们是先调用 when()
再调用 thenReturn()
方法,其实还可以先调用 doReturn()
再调用 when()
,这两种方式写法有些不同,但是效果是一样的。
测试方法在 4.1 StubbingTest
类中编写:
1 2 3 4 5 6 7 8 @Test public void testStubbingDoReturn () { Mockito.when(list.get(0 )).thenReturn("first" ); Mockito.doReturn("second" ).when(list).get(1 ); Assert.assertThat(list.get(0 ), CoreMatchers.equalTo("first" )); Assert.assertEquals(list.get(1 ), "second" ); }
4.4 迭代式 Stubbing
如果要实现第一次调用一个方法返回 a,第二次调用返回 b,第三次调用返回 c,这应该怎么做呢?
可以使用迭代式 Stubbing,这种方式有两种实现:
1、调用 thenReturn()
方法时,指定多个参数,不同调用次数得到的返回值与参数的顺序一致
2、多次调用 thenReturn()
方法,不同调用次数得到的返回值与调用 thenReturn()
方法的顺序一致
测试方法在 4.1 StubbingTest
类中编写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void testIterateStubbing () { Mockito.when(list.size()).thenReturn(1 ).thenReturn(2 ).thenReturn(3 ).thenReturn(4 ); Assert.assertEquals(list.size(), 1 ); Assert.assertEquals(list.size(), 2 ); Assert.assertEquals(list.size(), 3 ); Assert.assertEquals(list.size(), 4 ); Assert.assertEquals(list.size(), 4 ); }
4.5 thenAnswer
如果在测试的方法的返回值存在一定的逻辑处理关系,我们应该怎么测试呢?
就动态数组 ArrayList<String>
而言,get(n)
得到的结果总是 n * 10
,那应该怎么测试?
这里就可以用到 thenAnswer()
方法。
测试方法在 4.1 StubbingTest
类中编写:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void testStubbingWithAnswer () { Mockito.when(list.get(Mockito.anyInt())).thenAnswer(invocation -> { Integer index = invocation.getArgumentAt(0 , Integer.class); return String.valueOf(index * 10 ); }); for (int i = 0 ; i < 10 ; i++) { int num = (int )(Math.random() * 100 ) + 1 ; Assert.assertEquals(list.get(num), String.valueOf(num * 10 )); } }
4.6 real call
创建一个名为 StubbingService
的类,其内容为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class StubbingService { public int getI () { System.out.println("==== getI ====" ); return 10 ; } public String getS () { System.out.println("==== getS ====" ); throw new RuntimeException (); } }
在这节需要依赖这个类来做测试。
假设需要测试 StubbingService
中的两个方法,由于 getS()
方法中抛出了异常,所以肯定不能直接用 StubbingService
对象来调用 getS()
方法,需要使用 Mockito 来 mock 一个。
来测试一下 getI()
和 getS()
方法,看看他俩的测试结果是多少:
1 2 3 4 5 6 7 @Test public void testStubbingWithRealCall () { StubbingService service = Mockito.mock(StubbingService.class); System.out.println(service.getS()); System.out.println(service.getI()); }
运行后控制台输出:
null
0
虽然测试通过,但是 getI()
方法得到的结果并不是我们想要的,得到的结果是 0,期望的是 10,这是为什么呢?
看看 service
对象的 Class
是啥:
1 2 3 4 5 @Test public void testStubbingWithRealCall () { StubbingService service = Mockito.mock(StubbingService.class); System.out.println(service.getClass()); }
运行后控制台输出:
class indi.mofan.service.StubbingService$$EnhancerByMockitoWithCGLIB$$6681c1a6
看到了 CGLIB 的字样,这个对象是由 cglib 生成的代理对象,代理对象的 getI()
和 getS()
方法肯定不是原来的方法了。
那如果想测试 getS()
方法时能够测试通过,测试 getI()
方法时又能打印出原方法的结果,应该怎么办?
只需要使用 Stubbing 中的 thenCallRealMethod()
方法即可:
1 2 3 4 5 6 7 8 9 10 @Test public void testStubbingWithRealCall () { StubbingService service = Mockito.mock(StubbingService.class); Mockito.when(service.getS()).thenReturn("mofan" ); Assert.assertEquals(service.getS(), "mofan" ); Mockito.when(service.getI()).thenCallRealMethod(); Assert.assertEquals(service.getI(), 10 ); }
运行上述代码,测试通过,控制台输出:
==== getI ====
5. Mockito Spying
当我们 mock 一个对象时,这个对象时生成的代理对象,调用这个对象的方法并不是真实对象的方法,除非使用了前文提到的 thenCallRealMethod()
方法。
当我们 spy 一个对象时,可以称这个对象是真实对象的监控(spy)对象。当使用这个 spy 对象时真实的对象也会也调用,除非它的方法被 stubbing 了。可以理解成就和 mock 出的对象相反。
应当尽量少使用 spy 对象,使用时也需要小心。spy 对象可以用来处理遗留代码。
说起有点迷糊,来看一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @RunWith(MockitoJUnitRunner.class) public class SpyingTest { @Test public void testSpy () { List<String> realList = new ArrayList <>(); List<String> list = Mockito.spy(realList); list.add("mofan" ); list.add("默烦" ); Assert.assertEquals(list.get(0 ), "mofan" ); Assert.assertEquals(list.get(1 ), "默烦" ); Assert.assertEquals(list.size(), 2 ); Mockito.when(list.size()).thenReturn(100 ); Assert.assertTrue(list.size() != 2 ); Assert.assertEquals(list.size(), 100 ); } }
创建一个 spy 对象时,需要先创建一个真实对象,然后再使用 Mockito.spy()
方法,并将真实对象传入这个方法以创建一个 spy 对象。
上述创建 spy 对象一共有两步,那可不可以整合成一步呢?其实是可以的,可以使用注解的方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class SpyAnnotationTest { @Spy List<String> list = new ArrayList <>(); @Before public void init () { MockitoAnnotations.initMocks(this ); } @Test public void testSpyByAnnotation () { list.add("one" ); list.add("two" ); Assert.assertEquals(list.get(0 ), "one" ); Assert.assertEquals(list.get(1 ), "two" ); Mockito.when(list.size()).thenReturn(100 ); Assert.assertEquals(list.size(), 100 ); } }
还需要注意的是,有时候在监控对象上使用 when(Object)
来进行 Stubbing 是不切实际的(编译器直接报错)。因此,当使用监控对象时需要考虑使用 doReturn|Answer|Throw()
等方法来进行 Stubbing。例如:
1 2 3 4 5 6 7 8 9 10 11 @Test public void testSpyAndDoReturn () { List<String> realList = new ArrayList <>(); List<String> list = Mockito.spy(realList); Mockito.doReturn("mofan" ).when(list).get(0 ); Assert.assertEquals(list.get(0 ), "mofan" ); }
调用 list.get(0)
时会调用真实对象的 get(0)
方法,此时会发生 IndexOutOfBoundsException
异常,因为此时真实的 List 对象是空的,因此编译器中就直接报错了。
区分 mock 对象和 spy 对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void testDistinguishMockAndSpy () { List<String> realList = new ArrayList <>(); List<String> spyList = Mockito.spy(realList); List<String> mockList = Mockito.mock(ArrayList.class); boolean isSpy = Mockito.mockingDetails(spyList).isSpy(); boolean notMock = Mockito.mockingDetails(spyList).isMock(); boolean isMock = Mockito.mockingDetails(mockList).isMock(); boolean notSpy = Mockito.mockingDetails(mockList).isSpy(); Assert.assertTrue(isSpy); Assert.assertTrue(notMock); Assert.assertTrue(isMock); Assert.assertFalse(notSpy); }
上述测试方法可以通过。
因为 spy 对象只是 mock 对象的一种变种,所以对 spy 对象调用 isMock()
方法会返回 true
。
6. Argument Matchers
6.1 基本使用
Argument Matchers 又称参数匹配器。Mockito 以自然的 Java 风格来验证参数值,即:使用 equals()
函数。
比如很简单的参数匹配器的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class ArgumentMatcherTest { @Test @SuppressWarnings("unchecked") public void testBasic () { List<String> list = Mockito.mock(ArrayList.class); Mockito.when(list.get(0 )).thenReturn("mofan" ); Mockito.when(list.get(Mockito.eq(1 ))).thenReturn("默烦" ); Assert.assertEquals(list.get(0 ), "mofan" ); Assert.assertEquals(list.get(1 ), "默烦" ); Assert.assertNull(list.get(2 )); Mockito.verify(list).get(0 ); } }
6.2 isA() 与 any() 的使用
环境准备
在 ArgumentMatcherTest
类中添加以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 static class Foo { int function (Parent p) { return p.work(); } } interface Parent { int work () ; } static class Child1 implements Parent { @Override public int work () { throw new RuntimeException (); } } static class Child2 implements Parent { @Override public int work () { throw new RuntimeException (); } }
测试 Mockito.isA()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Test public void testIsA_1 () { Foo foo = Mockito.mock(Foo.class); Mockito.when(foo.function(Mockito.isA(Parent.class))) .thenReturn(100 ); Mockito.when(foo.function(Mockito.isA(Child1.class))) .thenReturn(200 ); int result_1 = foo.function(new Child1 ()); int result_2 = foo.function(new Child2 ()); Assert.assertEquals(result_1, 200 ); Assert.assertEquals(result_2, 100 ); } @Test public void testIsA_2 () { Foo foo = Mockito.mock(Foo.class); Mockito.when(foo.function(Mockito.isA(Child1.class))) .thenReturn(200 ); int result_1 = foo.function(new Child1 ()); int result_2 = foo.function(new Child2 ()); Assert.assertEquals(result_1, 200 ); Assert.assertEquals(result_2, 0 ); }
对于 Mockito.isA(Parent.class)
来说,调用 function()
方法传递的参数是 Parent 的实例就可以匹配成功。
对于 Mockito.isA(Child1.class)
来说,调用 function()
方法传递的参数是 Child1 的实例就可以匹配成功。由于 new Child2()
不是 Child1 的实例,因此调用 function()
方法传入 new Child2()
时,得到的返回值是 int
类型的默认值。
Mockito.isA()
方法的源码:
1 2 3 public static <T> T isA (Class<T> clazz) { return reportMatcher(new InstanceOf (clazz)).<T>returnFor(clazz); }
测试 Mockito.any()
1 2 3 4 5 6 7 8 9 @Test public void testAny () { Foo foo = Mockito.mock(Foo.class); Mockito.when(foo.function(Mockito.any(Child1.class))) .thenReturn(100 ); Assert.assertEquals(foo.function(new Child1 ()), 100 ); Assert.assertEquals(foo.function(new Child2 ()), 100 ); }
上述代码可以测试通过,感觉和 isA()
好像区别很大? ⁉️ 看看它的源码!
Mockito.any()
方法的源码:
1 2 3 public static <T> T any (Class<T> clazz) { return (T) reportMatcher(Any.ANY).returnFor(clazz); }
在 reportMatcher()
方法中使用了 Any.ANY
,来看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Any extends ArgumentMatcher implements Serializable { private static final long serialVersionUID = -4062420125651019029L ; public static final Any ANY = new Any (); private Any () {} public boolean matches (Object actual) { return true ; } public void describeTo (Description description) { description.appendText("<any>" ); } }
从源码中很容易明白,any()
是一个泛型方法,这个方法的返回值类型和其传入参数类型的泛型一致,在调用 function()
方法时,会进行语法匹配(或者说语法检查),要求 any()
方法的参数泛型和传入 function()
方法的参数类型一致。如果我们能绕过这个语法检查,那么传入任何参数都行。
6.3 Wildcard Matchers
Wildcard Matchers 称为通配符匹配器。这有什么用呢?
针对 List<String> list = Mockito.mock(ArrayList.class);
来说:
如果要 Stubbing ,可以这样:
1 Mockito.when(list.get(0 )).thenReturn("mofan" );
那如果我想调用 get()
方法时,传入 0 到 99 共一百个数时,都返回 mofan 字符串,难道要写一百行?
这个时候就可以使用通配符匹配器。
在前文中我们已经接触过通配符匹配器了,比如:Mockito.anyInt()
、Mockito.anyString()
等等都属于通配符匹配器。
需要注意的是: 如果我们在 Stubbing 中某个方法的某个参数使用了通配符匹配器,那么所有参数都要使用通配符解析器,否则测试不会通过!
环境准备
1 2 3 4 5 6 7 8 9 10 11 12 13 public class SimpleService { public int method1 (int i, String s, Collection<?> c, Serializable ser) { throw new RuntimeException (); } public void method2 (int i, String s, Collection<?> c, Serializable ser) { throw new RuntimeException (); } }
使用通配符匹配器
为使代码更清晰简洁,本次代码将采用静态导包的方式。
简单使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import static org.mockito.Matchers.anyCollection;import static org.mockito.Matchers.anyInt;import static org.mockito.Matchers.anyString;import static org.mockito.Matchers.eq;import static org.mockito.Mockito.when;@RunWith(MockitoJUnitRunner.class) public class WildcardArgumentMatcherTest { @Mock private SimpleService simpleService; @After public void destroy () { Mockito.reset(simpleService); } @Test public void testWildcardMethod1 () { when(simpleService.method1(anyInt(), anyString(), anyCollection(), Mockito.isA(Serializable.class))).thenReturn(100 ); int result_1 = simpleService.method1(666 , "mofan" , Collections.emptyList(), "默烦" ); Assert.assertEquals(result_1, 100 ); int result_2 = simpleService.method1(888 , "默烦" , Collections.emptySet(), "mofan" ); Assert.assertEquals(result_2, 100 ); } }
如果在某些情况下返回特殊的值,非特殊情况下返回一般的值,应该这样做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test public void testWildcardMethod1WithSpec () { when(simpleService.method1(anyInt(), anyString(), anyCollection(), Mockito.isA(Serializable.class))).thenReturn(-1 ); when(simpleService.method1(anyInt(), eq("mofan" ), anyCollection(), Mockito.isA(Serializable.class))).thenReturn(100 ); when(simpleService.method1(anyInt(), eq("默烦" ), anyCollection(), Mockito.isA(Serializable.class))).thenReturn(200 ); int result_1 = simpleService.method1(111 , "mofan" , Collections.emptyList(), "mofan" ); int result_2 = simpleService.method1(111 , "默烦" , Collections.emptyList(), "默烦" ); int result_3 = simpleService.method1(111 , "qwer" , Collections.emptyList(), "默烦" ); Assert.assertEquals(result_1, 100 ); Assert.assertEquals(result_2, 200 ); Assert.assertEquals(result_3, -1 ); }
注意上述代码中的多行注释: 第一次 Stubbing 时,使用了通配符匹配器 anyString()
,后面两次 Stubbing 指定了特殊的值,相当于后两次的 Stubbing 是第一次 Stubbing 中的特殊情况。如果将第一句 Stubbing 移动到第三句,由于 anyString()
的范围比指定的特殊情况的范围大得多,将会把前两次 Stubbing cover(覆盖)掉,因此测试无法通过。
没有返回值的方法应该这样做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test public void testWildcardMethod2 () { List<Object> emptyList = Collections.emptyList(); Mockito.doNothing().when(simpleService).method2(anyInt(), anyString(), anyCollection(), Mockito.isA(Serializable.class)); simpleService.method2(666 , "mofan" , emptyList, "默烦" ); Mockito.verify(simpleService, Mockito.times(1 )) .method2(666 , "mofan" , emptyList, "默烦" ); Mockito.verify(simpleService, Mockito.times(1 )) .method2(anyInt(), eq("mofan" ), anyCollection(), Mockito.isA(Serializable.class)); }
6.4 Hamcrest Matchers
Hamcrest 是一个 Java 类库的名字,是一种测试辅助工具,它提供了大量被称为“匹配器”的方法。其中每个匹配器都设计用于执行特定的比较操作。Hamcrest 的可扩展性很好,让使用者能够创建自定义的匹配器。最重要的是,JUnit 也包含了 Hamcrest 的核心,提供了对 Hamcrest 的原生支持,可以直接使用 Hamcrest。
当然要使用功能齐备的 Hamcrest,还是得导入依赖。
Hamcrest 的使用非常广泛,能在很多地方看到它,比如:JUnit、Spark、Hadoop、Flume 等等,而 Mockito 中的 Matcher 也是由 Hamcrest 提供的。
Hamcrest 官网:Hamcrest
前文中,使用断言 Assert 进行验证时,都使用的 assertEquals()
、assertTrue()
等类似的方法,其实不建议这样做,这种写法有很大的局限性。如果满足期望值中的某一个就测试通过,又或者不满足所有期望值才测试通过,这些情况该怎么验证呢?
因此,建议使用 Assert.assertThat()
方法!
使用示例
为使代码更清晰简洁,本次代码将采用静态导包的方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import static org.hamcrest.CoreMatchers.allOf;import static org.hamcrest.CoreMatchers.anyOf;import static org.hamcrest.CoreMatchers.both;import static org.hamcrest.CoreMatchers.either;import static org.hamcrest.CoreMatchers.equalTo;import static org.hamcrest.CoreMatchers.not;import static org.hamcrest.CoreMatchers.is;public class AssertMatcherTest { @Test public void test1 () { int i = 10 ; Assert.assertThat(i, equalTo(10 )); Assert.assertThat(i, not(equalTo(20 ))); Assert.assertThat(i, is(10 )); Assert.assertThat(i, not(is(20 ))); Assert.assertThat(i, is(not(20 ))); } }
有没有很优雅?这种书写方式简洁明了还优雅。
那怎么使用最开始提出的需求呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void test2 () { double price = 2.12 ; Assert.assertThat(price, either(equalTo(2.12 )).or(equalTo(1.12 ))); Assert.assertThat(price, both(not(equalTo(1.12 ))).and(not(equalTo(2.11 )))); Assert.assertThat(price, anyOf(is(2.12 ), not(1.12 ), is(6.20 ))); Assert.assertThat(price, allOf(is(2.12 ), not(is(1.12 )), not(2.11 ))); Assert.assertThat(Stream.of(1 , 2 , 3 ).allMatch(i -> i > 0 ), equalTo(true )); }
使用 Assert.assertThat()
甚至还可以指定校验错误原因:
1 2 3 4 5 6 7 @Test public void test3 () { double price = 2.12 ; Assert.assertThat("the double value assertion failed" , price, either(equalTo(2.22 )).or(equalTo(1.12 ))); }
上述代码很显然不能通过测试,运行后,控制台会显示我们指定的信息:
java.lang.AssertionError: the double value assertion failed
Expected: (<2.22> or <1.12>)
but: was <2.12>
出现的错误
在初次运行 test3 时,虽然测试确实没有通过,但是控制台出现的信息是:
java.lang.NoSuchMethodError: org.hamcrest.Matcher.describeMismatch(Ljava/lang/Object;Lorg/hamcrest/Description;)V
这显然不是我们想要的。
这主要是因为 Junit 中的 Hamcrest 的问题,或者说是因为最初导入依赖时,导入了 mockito-all 依赖的问题。
除此之外,在编写 test2 时,也会遇到静态导入了 org.hamcrest.CoreMatchers.either
,但却找不到的问题, 这是因为 mockito-all 依赖中的 CoreMatchers
类里根本就没有 either()
方法。
解决上面两个问题很简单,再导入 hamcrest-core 依赖就可以了。
但需要注意的是: 确保 hamcrest 依赖在导入顺序上高于JUnit 依赖。粗暴一点, 直接让 hamcrest 依赖的导入顺序是第一个,因为 JUnit 和 Mockito 中都有 Hamcrest 的类,而这些类可能正在被使用。比如我的依赖是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <dependencies > <dependency > <groupId > org.hamcrest</groupId > <artifactId > hamcrest-core</artifactId > <version > 2.2</version > <scope > test</scope > </dependency > <dependency > <groupId > org.mockito</groupId > <artifactId > mockito-all</artifactId > <version > 1.10.19</version > <scope > test</scope > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.12</version > <scope > test</scope > </dependency > <dependency > <groupId > javax.servlet</groupId > <artifactId > javax.servlet-api</artifactId > <version > 3.1.0</version > <scope > provided</scope > </dependency > </dependencies >
6.5 自定义匹配器
匹配器也可以自定义,比如 Hamcrest 的 CoreMatchers
类中并没有 lt()
或 gt()
方法,我们可以自己自定义这些方法。
首先说明: 我们在此只比较基本数据类型的数字。
为了便于后续拓展,可以这样做:
创建一个 Compare
接口,对行为进行抽象:
1 2 3 4 5 6 7 8 package indi.mofan.utils;public interface Compare <T extends Number > { boolean compare (T expected, T actual) ; }
编写 DefaultNumberCompare
实现类,实现比较逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package indi.mofan.utils;public class DefaultNumberCompare <T extends Number > implements Compare <T> { private final boolean greater; public DefaultNumberCompare (boolean greater) { this .greater = greater; } @Override public boolean compare (T expected, T actual) { Class<?> clazz = actual.getClass(); if (clazz == Integer.class) { return greater ? ((Integer) actual) > ((Integer) expected) : ((Integer) actual) < ((Integer) expected); } else if (clazz == Short.class) { return greater ? ((Short) actual) > ((Short) expected) : ((Short) actual) < ((Short) expected); } else if (clazz == Byte.class) { return greater ? ((Byte) actual) > ((Byte) expected) : ((Byte) actual) < ((Byte) expected); } else if (clazz == Double.class) { return greater ? ((Double) actual) > ((Double) expected) : ((Double) actual) < ((Double) expected); } else if (clazz == Float.class) { return greater ? ((Float) actual) > ((Float) expected) : ((Float) actual) < ((Float) expected); } else if (clazz == Long.class) { return greater ? ((Long) actual) > ((Long) expected) : ((Long) actual) < ((Long) expected); } else { throw new AssertionError ("The number type" + clazz + "not supported" ); } } }
编写 CompareNumberMatcher
自定义数字匹配器,完成 gt()
和 lt()
方法的编写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package indi.mofan.utils;import org.hamcrest.BaseMatcher;import org.hamcrest.Description;import org.hamcrest.Factory;public class CompareNumberMatcher <T extends Number > extends BaseMatcher <T> { private final T value; private final Compare<T> COMPARE; public CompareNumberMatcher (T value, boolean greater) { this .COMPARE = new DefaultNumberCompare <>(greater); this .value = value; } @Override public boolean matches (Object actual) { return this .COMPARE.compare(value, (T) actual); } @Factory public static <T extends Number > CompareNumberMatcher<T> gt (T value) { return new CompareNumberMatcher <>(value, true ); } @Factory public static <T extends Number > CompareNumberMatcher<T> lt (T value) { return new CompareNumberMatcher <>(value, false ); } @Override public void describeTo (Description description) { description.appendText("compare two number failed." ); } }
测试一下,看看自定义匹配器能否使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package indi.mofan.matchers;import org.junit.Assert;import org.junit.Test;import static indi.mofan.utils.CompareNumberMatcher.gt;import static indi.mofan.utils.CompareNumberMatcher.lt;import static org.hamcrest.CoreMatchers.both;public class CustomMatcherTest { @Test public void test1 () { Assert.assertThat(10 , gt(5 )); Assert.assertThat(10 , lt(20 )); Assert.assertThat(10 , both(gt(5 )).and(lt(20 ))); } }
运行后,测试通过,非常完美! 🎉
7. Mockito Verify
前文中,我们已经使用了很多次静态导包了,从这里开始, 后续贴出的代码都将使用静态导包, 让代码更加简洁。为了避免同名方法造成的混淆,在此列出本节涉及的所有静态导包:
1 2 3 4 5 6 7 8 9 10 11 12 13 import static org.mockito.Mockito.atLeast;import static org.mockito.Mockito.atLeastOnce;import static org.mockito.Mockito.atMost;import static org.mockito.Mockito.calls;import static org.mockito.Mockito.ignoreStubs;import static org.mockito.Mockito.inOrder;import static org.mockito.Mockito.mock;import static org.mockito.Mockito.never;import static org.mockito.Mockito.times;import static org.mockito.Mockito.verify;import static org.mockito.Mockito.verifyNoMoreInteractions;import static org.mockito.Mockito.verifyZeroInteractions;import static org.mockito.Mockito.when;
7.1 验证某些行为
基本使用
前文中说了 Stubbing,其实 mock 对象一被创建,mock 对象就会记住所有的交互,然后我们就可以选择性的验证我们感兴趣的交互。比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testBasicVerify () { List<String> list = mock(ArrayList.class); list.add("mofan" ); list.clear(); verify(list).add("mofan" ); verify(list).clear(); verify(list, times(1 )).clear(); }
如果在上述测试代码中添加一行以下代码:
由于我们并没有添加字符串 “默烦” 进 list 中,因此在运行测试代码就会报错:
Wanted but not invoked:
arrayList.add("默烦");
验证参数匹配器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @RunWith(MockitoJUnitRunner.class) public class VerifyArgumentMatcherTest { @Mock private SimpleService simpleService; @After public void destroy () { Mockito.reset(simpleService); } @Test public void testVerifyArgumentMatcher () { when(simpleService.method1(anyInt(), anyString(), anyCollection(), isA(Serializable.class))).thenReturn(100 ); simpleService.method1(666 , "mofan" , Collections.emptyList(), "默烦" ); verify(simpleService).method1(anyInt(), anyString(), anyCollection(), eq("默烦" )); } }
像 anyObject()
,eq()
这样的匹配器方法不会返回匹配器。它们会在内部将匹配器记录到一个栈当中,并且返回一个假的值,通常为 null
。此实现归因于Java 编译器施加的静态类型安全性。其结果就是不能在验证或者 Stubbing 方法之外使用 anyObject()
,eq()
等方法。
7.2 验证方法执行次数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Test public void testVerifyExecutionTimes () { List<String> list = mock(ArrayList.class); list.add("once" ); list.add("twice" ); list.add("twice" ); list.add("third" ); list.add("third" ); list.add("third" ); verify(list).add("once" ); verify(list, times(2 )).add("twice" ); verify(list, times(3 )).add("third" ); verify(list, never()).add("mofan" ); verify(list, atLeastOnce()).add("third" ); verify(list, atLeast(2 )).add("twice" ); verify(list, atMost(5 )).add("third" ); }
7.3 验证执行顺序
验证单个 mock 对象的方法的执行顺序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void testVerifySingleOrder () { List<String> list = mock(ArrayList.class); list.add("was added first" ); list.add("was added first" ); list.add("was added first" ); list.add("was added second" ); InOrder inOrder = inOrder(list); inOrder.verify(list, calls(2 )).add("was added first" ); inOrder.verify(list).add("was added second" ); }
上述代码能够测试通过。
这里使用了 calls()
方法,该方法只能在验证执行顺序时使用。这个方法如果调用 3 次不会失败,不同于 times(2)
,并且也不会标记第三次验证,不同于 atLeast(2)
。
验证多个 mock 对象的方法的执行顺序
如果有多个 mock 对象,只需要在创建 InOrder 对象时将那些 mock 对象传进 Mockito.inOrder()
方法即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testVerifyMultipleOrder () { List<String> firstList = mock(ArrayList.class); List<String> secondList = mock(ArrayList.class); firstList.add("was called first" ); secondList.add("was called second" ); InOrder inOrder = inOrder(firstList, secondList); inOrder.verify(firstList).add("was called first" ); inOrder.verify(secondList).add("was called second" ); }
验证执行顺序是非常灵活的。我们并不需要一个一个的验证所有交互,只需要验证我们想要验证的对象即可,通过那些需要验证顺序的 mock 对象来创建 InOrder
对象就行了。
7.4 verifyZeroInteractions
我们还可以验证某个或者某些 mock 对象是否进行过交互。调用一次 mock 对象的方法,然后验证调用这个方法就成为一次交互。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void testVerifyInteraction () { List<String> firstList = mock(ArrayList.class); List<String> secondList = mock(ArrayList.class); List<String> thirdList = mock(ArrayList.class); firstList.add("mofan" ); secondList.add("one" ); secondList.add("two" ); verify(firstList).add("mofan" ); verify(secondList, never()).add("默烦" ); verifyZeroInteractions(firstList, thirdList); }
7.5 verifyNoMoreInteractions
我们还可以验证调用 mock 对象的所有方法是否都进行了验证。比如:
1 2 3 4 5 6 7 8 9 10 @Test public void testNoMoreInteraction_1 () { List<String> list = mock(ArrayList.class); list.add("mofan" ); verifyNoMoreInteractions(list); }
运行上述测试方法,验证不会通过,控制台打印:
org.mockito.exceptions.verification.NoInteractionsWanted:
No interactions wanted here:
如果我们对 add("mofan")
加上验证,测试方法就会通过,比如:
1 2 3 4 5 6 7 8 9 10 11 @Test public void testNoMoreInteraction_2 () { List<String> list = mock(ArrayList.class); list.add("mofan" ); verify(list).add("mofan" ); verifyNoMoreInteractions(list); }
只要有一个方法没验证,测试方法都不会通过,比如下述测试方法也不会通过:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testNoMoreInteraction_3 () { List<String> list = mock(ArrayList.class); list.add("one" ); list.add("two" ); list.add("three" ); verify(list).add("one" ); verify(list).add("two" ); verifyNoMoreInteractions(list); }
但是并不建议频繁地使用verifyNoMoreInteractions()
,甚至在每个测试函数中都用。verifyNoMoreInteractions()
在交互测试套件中只是一个便利的验证,它是当你需要验证是否存在冗余调用时才使用。滥用它将导致测试代码的可维护性降低。
通常认为 never()
是一种更为明显且易于理解的形式。
7.6 验证被忽略的 Stubbing
Mockito 现在允许为了验证无视测试桩。比如下述测试方法运行后能通过:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void testIgnoreStubbing () { List<Integer> firstList = mock(ArrayList.class); List<Integer> secondList = mock(ArrayList.class); when(firstList.get(0 )).thenReturn(10 ); when(secondList.get(0 )).thenReturn(20 ); Assert.assertThat(firstList.get(0 ), CoreMatchers.equalTo(10 )); Assert.assertThat(secondList.get(0 ), CoreMatchers.equalTo(20 )); verifyNoMoreInteractions(ignoreStubs(firstList, secondList)); }
当然,还可以在 InOrder 对象中忽略 Stubbing,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void testIgnoreInOrder () { List<Integer> list = mock(ArrayList.class); when(list.get(0 )).thenReturn(100 ); list.add(0 ); list.clear(); System.out.println(list.get(0 )); InOrder inOrder = inOrder(ignoreStubs(list)); inOrder.verify(list).add(0 ); inOrder.verify(list).clear(); inOrder.verifyNoMoreInteractions(); }
7.7 验证超时时间
Mockito 允许带有暂停的验证。这使得一个验证去等待一段特定的时间,以获得想要的交互而不是如果还没有发生事件就带来的立即失败。
这在并发条件下的测试这会很有用,这里就直接贴出官网的代码吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 verify(mock, timeout(100 )).someMethod(); verify(mock, timeout(100 ).times(1 )).someMethod(); verify(mock, timeout(100 ).times(2 )).someMethod(); verify(mock, timeout(100 ).atLeast(2 )).someMethod(); verify(mock, new Timeout (100 , yourOwnVerificationMode)).someMethod();
7.8 自定义验证失败信息
与 Assert.assertThat()
一样,Mockito.verify()
也允许自定义验证失败信息。比如:
1 2 3 4 5 verify(mock, description("This will print on failure" )).someMethod(); verify(mock, times(2 ).description("someMethod should be called twice" )).someMethod();
但需要注意的是,这个特性在 2.0.0 之后才存在。
8. Arguments Captor
8.1 参数捕获
在前面,介绍了参数匹配器,其实参数还能够被我们捕获,接下来介绍一下如何捕获参数。因为在某些情况下,当验证交互之后要检测真实的参数值时参数捕获就变得有用起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import static org.mockito.Mockito.mock;import static org.mockito.Mockito.times;import static org.mockito.Mockito.verify;public class ArgumentCaptorTest { @Test @SuppressWarnings("unchecked") public void testCaptureArgument () { List<String> list = Arrays.asList("1" , "2" ); List<String> mockedList = mock(ArrayList.class); ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class); mockedList.addAll(list); verify(mockedList).addAll(argument.capture()); Assert.assertEquals(2 , argument.getValue().size()); Assert.assertEquals(list, argument.getValue()); } }
上述代码中通过 verify(mockedList).addll(argument.capture())
语句来获取 mockedList.addAll()
方法所传递的实参 list
。
再看看下面这个测试方法,相信它能让你对参数捕获理解更透彻:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void test () { List<Integer> list = mock(ArrayList.class); ArgumentCaptor<Integer> argument = ArgumentCaptor.forClass(Integer.class); list.add(1 ); int temp = ThreadLocalRandom.current().nextInt(1000 ); list.add(temp); verify(list, times(2 )).add(argument.capture()); Assert.assertEquals(temp, argument.getValue().intValue()); Assert.assertTrue(argument.getAllValues().contains(temp)); Assert.assertTrue(argument.getAllValues().contains(1 )); }
根据官方文档: 建议使用没有 Stubbing 的 ArgumentCaptor
来验证,因为使用含有 Stubbing 的 ArgumentCaptor
会降低测试代码的可读性,因为 captor 是在断言代码块之外创建的。另一个好处是它可以降低本地化的缺点,因为如果 Stubbing 函数没有被调用,那么参数就不会被捕获。
使用 ArgumentCaptor
在以下的情况下更合适:
8.2 一些注解
前面也介绍了类似于 @Mock
这样的注解,我们需要明白,这些注解并不是一添加就生效,要想让这些注解生效,需要:
在测试类上使用:@RunWith(MockitoJUnitRunner.class)
在 @Before 中调用:MockitoAnnotations.initMocks(this)
在类中定义:@Rule public MockitoRule mockito = MockitoJUnit.rule();
在这里,我们将会介绍以下注解:
@Captor
@InjectMocks
@MockBean / @SpyBean
@Captor
@Captor
注解可以获取 Matcher 实际执行时对应的参数,相当于简化 ArgumentCaptor
的创建。比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import static org.mockito.Mockito.mock;import static org.mockito.Mockito.verify;@RunWith(MockitoJUnitRunner.class) public class ArgumentCaptorAnnotationTest { @Captor ArgumentCaptor<List> captor; @Test public void testCaptureArgument () { List<String> list = Arrays.asList("1" , "2" ); List<String> mockedList = mock(ArrayList.class); mockedList.addAll(list); verify(mockedList).addAll(captor.capture()); Assert.assertEquals(2 , captor.getValue().size()); Assert.assertEquals(list, captor.getValue()); } }
@InjectMocks
@InjectMocks
:创建一个实例,其余用 @Mock
(或 @Spy
)注解创建的 mock 将被注入到用该实例中。
有点类似 Spring 中的 @Autowried
,具体使用方式参考:@InjectMocks
@MockBean
/ @SpyBean
@MockBean/@SpyBean:相对于 @Mock / @Spy,并且此注释的对象,被加入到 Spring 容器中。
9. Integration with SpringBoot
9.1 整合 Mockito
在前文中,我们已经介绍了 Mockito 的基本使用,但是在实际开发过程中,我们面对的代码和业务更复杂,不会使用上述 Demo 级别的代码进行测试。通常来说,我们会选择与 Spring 或 SpringBoot 进行整合,那么我们在此简单介绍一下。
在实际开发中,一个类往往会依赖多个类,这个时候如果我们需要测试就会很麻烦,这个时候 Mockito 就应运而生了。
为了增加代码的简洁性,我们选择使用 @Mock
注解来 mock 对象。所以别忘记使用 @RunWith
注解或者在 @Before
注解标记的方法内添加 MockitoAnnotations.initMocks(this);
。
在对某个类 A 进行测试时,A 中依赖了 B,我们可以 mock 一个 B 将其注入到 A 中,这时需要使用到 @InjectMocks
和 @Mock
注解。真正被测试的类使用 @InjectMocks
注解,被依赖的类使用 @Mock
进行 mock,这样就可以将 mock 出的对象注入到被 @InjectMocks
注解标记的类。
如果在整个测试中,我们还使用了 Spring 的 @Autowired
注解来进行依赖注入,这时候需要在使用了 Mockito 的测试类上添加 @RunWith(SpringJUnit4ClassRunner.class)
,否则会注入失败导致报错。
@InjectMocks 与 @Mock 的选择
参考链接:mock测试及jacoco覆盖率
真正需要测试的类,要用 @InjectMocks
,而不是 @Mock
(更不能是 @Autowired
),原因如下:
原因 1:@Autowired
是 Spring 的注解,在 mock 环境下,根本就没有 Spring 上下文,当然会注入失败。
原因 2:也不能是 @Mock
,@Mock
表示该注入的对象是“虚构”的假对象,里面的方法代码根本不会真正运行,统一返回空对象 null
,即:被 @Mock
修饰的对象,在该测试类中,其具体的代码永远无法覆盖到!这也就是失败了单元测试的意义。而 @InjectMocks
修饰的对象,被测试的方法,才会真正进入执行。
另外,测试服务时,被 mock 注入的类,应该是具体的服务实现类,即:xxxServiceImpl,而不是服务接口,在 mock 环境中接口是无法实例化的。
9.2 整合 PowerMock
关于 PowerMock 的内容,可在本站搜索【PowerMock 实战】进行查看,再次列举一下 PowerMock 整合 SpringBoot 用到的一些注解。
参考链接:Spring的Mock测试你用上了吗?
@RunWith(PowerMockRunner.class)
:假如要使用 Powermock,需要在测试类上添加该注解,不然 Powermock 无法使用。
@PrepareForTest
:参考【PowerMock 实战】一文中 PowerMock 的增强 。
@PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class)
: 如果在测试类上只添加了以上两个注解,会发现从 Spring 中 inject 注入的 Service 都为空了。需要使用这个注解与 Spring Test 整合。
@PowerMockIgnore({"javax.net.ssl.\*","javax.management.\*","javax.security.\*","javax.crypto.\*"})
:参考【PowerMock 实战】一文中 整合 Spring 。
PowerMock 整合 SpringBoot 和 Mockito 整合 SpringBoot 是类似的,因为 PowerMock 可以理解为对 Mockito 的增强。
10. Mock Static Methods
从 Mockito 2.1.0 开始可以通过 mockto-extensions 扩展的方式 Mock final 类和 final 方法,从 Mockito 3.4.0 通过类似的方式实现了对静态方法的 Mock。在最新的 4.3.1 版本中,这个功能已经孵化成功。
假设已经导入以下依赖:
1 2 3 4 5 6 <dependency > <groupId > org.mockito</groupId > <artifactId > mockito-core</artifactId > <version > 4.3.1</version > <scope > test</scope > </dependency >
如果想要 Mock 静态方法,可以将上述依赖修改为:
1 2 3 4 5 6 <dependency > <groupId > org.mockito</groupId > <artifactId > mockito-inline</artifactId > <version > 4.3.1</version > <scope > test</scope > </dependency >
实质上 mockito-inline
就是给 mockito-core
添加了两个插件配置,分别是 org.mockito.plugins.MockMaker
和 org.mockito.plugins.MemberAccessor
,前者也是用于 Mock final 类和 final 方法的。
因此在已经引入了 mockito-core 3.4.0 及其以上版本的情况下,如果不想将原依赖修改为 mockito-inline,可以在 classpath 下创建相同的文件及其内容也能够成功 Mock 静态方法。
比如:
org.mockito.plugins.MockMaker 文件中的信息:
org.mockito.plugins.MemberAccessor 文件中的信息:
mock-maker-inline
同时支撑了对 final 类,final 方法和静态方法的 Mock,而 member-accessor-module
控制了对测试类成员变量的访问。
Mock 无参静态方法
已存在如下类:
1 2 3 4 5 6 7 8 9 10 11 public class TestUtil { public static String generateHello (String name) { return String.format("Hello, %s" , name); } public static String helloWorld () { return "Hello, World" ; } }
如果需要对 helloWorld()
进行 Mock,可以:
1 2 3 4 5 6 7 8 9 10 11 @Test public void testNoParamStaticMethod () { MockedStatic<TestUtil> mockStatic = Mockito.mockStatic(TestUtil.class); mockStatic.when(TestUtil::helloWorld).thenReturn("Hello, Mofan" ); Assert.assertEquals(TestUtil.helloWorld(), "Hello, Mofan" ); Assert.assertNull(TestUtil.generateHello("boy" )); mockStatic.close(); }
对被测试类进行 Mockito.mockStatic()
后,被测试类内部所有方法都将返回 null
,这与 Mockito.mock()
是一样的。
如果在一个测试方法中进行了 Mockito.mockStatic()
,需要在方法末尾注销注册的静态 Mock。
Mock 有参静态方法
1 2 3 4 5 6 7 8 9 @Test public void testHaveParamStaticMethod () { MockedStatic<TestUtil> mockStatic = Mockito.mockStatic(TestUtil.class); mockStatic.when(() -> TestUtil.generateHello("boy" )).thenReturn("Hello, girl" ); Assert.assertEquals(TestUtil.generateHello("boy" ), "Hello, girl" ); mockStatic.verify(() -> TestUtil.generateHello(Mockito.anyString()), Mockito.times(1 )); mockStatic.close(); }
为什么要注销注册的静态 Mock
当一个测试类中多个测试方法中都对同一个类进行了静态 Mock, 并且测试方法末尾没有注销注册的静态 Mock,直接运行这个测试时,就会抛出类似如下异常:
org.mockito.exceptions.base.MockitoException:
For indi.mofan.domain.util.TestUtil, static mocking is already registered in the current thread
To create a new mock, the existing static mock registration must be deregistered
除此之外,如果对某个项目进行代码覆盖率测试时,在 A 测试类中对某个类中的静态方法进行了 Mock,且没有注销注册的静态 Mock,那么在 B 测试类中调用这个静态方法时会受到 Mock 的影响,进而返回由 Mockito 指定的值,而非本来的值。(我同事就被我坑过 🤪)
优雅地注销注册的静态 Mock
注销注册的静态 Mock 很重要,但在编码时很有可能会忘记掉这件事。为此,可以使用 try-with-resources
语句对 MockedStatic
进行自动注销。
摘自菜鸟教程 Java 9 改进的 try-with-resources :
try-with-resources
是 JDK 7 中一个新的异常处理机制,它能够很容易地关闭在 try-catch
语句块中使用的资源。所谓的资源(resource)是指在程序完成后,必须关闭的对象。try-with-resources
语句确保了每个资源在语句结束时关闭。所有实现了 java.lang.AutoCloseable
接口(其中,它包括实现了 java.io.Closeable
的所有对象),可以使用作为资源。
MockedStatic
接口继承了 java.lang.AutoCloseable
接口。
1 2 3 4 5 6 7 8 9 @Test public void testTryWithResources () { Assert.assertEquals(TestUtil.generateHello("boy" ), "Hello, boy" ); try (MockedStatic<TestUtil> mockStatic = Mockito.mockStatic(TestUtil.class)) { mockStatic.when(() -> TestUtil.generateHello("boy" )).thenReturn("Hello, girl" ); Assert.assertEquals(TestUtil.generateHello("boy" ), "Hello, girl" ); } Assert.assertEquals(TestUtil.generateHello("boy" ), "Hello, boy" ); }
推荐在实际开发中使用上述方式进行静态方法的 Mock。
针对上述代码,还有几点需要注意:
1、Mock 的静态方法只在 try-with-resources
块中可见。也就是说,在 try-with-resources
块外静态方法的执行将按照原本的结果返回,在 try-with-resources
块内静态方法的执行将按照 Mock 的结果返回;
2、不同的测试用例应该在不同的 try-with-resources
块中进行测试;
3、静态方法只能使用内联的方式来 Mock。