封面来源:本文封面来源于网络,如有侵权,请联系删除。

本文参考:汪文君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 的大致流程如下:

  • 创建外部依赖的 Mock 对象,然后将此Mock 对象注入到测试类中

  • 执行测试代码

  • 校验测试代码是否执行正确

为什么要使用 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
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>xxxx</version>
<scope>test</scope>
</dependency>

项目结构与代码准备

创建一个没有任何属性的实体类:

1
2
3
4
5
/**
* @author mofan 2020/12/18
*/
public class Account {
}

编写 Dao 层的代码,假设 DB 不存在,直接抛出异常:

1
2
3
4
5
6
7
8
9
10
/**
* @author mofan 2020/12/18
*/
public class AccountDao {

public Account findAccount(String username, String password) {
// 假设此时 DB 不可用
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
/**
* @author mofan 2020/12/18
*/
public class AccountLoginController {

private final AccountDao accountDao;

public AccountLoginController(AccountDao accountDao) {
this.accountDao = accountDao;
}

/**
* 模拟用户登录
* @return 界面名称
*/
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
/**
* @author mofan 2020/12/18
*/
@RunWith(MockitoJUnitRunner.class)
public class AccountLoginControllerTest {
private AccountDao accountDao;
private HttpServletRequest request;
private AccountLoginController accountLoginController;

@Before
public void before() {
// 测试方法执行前 Mock 数据
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");
// 指定 findAccount() 方法返回 null
Mockito.when(accountDao.findAccount(Matchers.anyString(), Matchers.anyString())).thenReturn(null);
// 返回 /login
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");
// 指定 findAccount() 方法返回 null
Mockito.when(accountDao.findAccount(Matchers.anyString(), Matchers.anyString()))
.thenThrow(UnsupportedOperationException.class);
// 返回 /login
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;
// 省略其他导入的包
/**
* @author mofan 2020/12/18
*/
@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
/**
* @author mofan 2020/12/18
*/
public class MockByAnnotationTest {

@Mock
private AccountDao accountDao;

@Before
public void init() {
MockitoAnnotations.initMocks(this);
}

@Test
public void testMock() {
Account account = accountDao.findAccount("x", "x");
// null
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");
// null
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;

/**
* @author mofan 2020/12/18
*/
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");
// null
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() {
// AccountDao accountDao = Mockito.mock(AccountDao.class);
Account account = accountDao.findAccount("x", "x");
// null
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
/**
* @author mofan 2020/12/18
*/
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
/**
* @author mofan 2020/12/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
/**
* @author mofan 2020/12/18
*/
public class DeepMockTest {
@Mock
private UserService userService;

@Mock
private User user;

@Before
public void init() {
MockitoAnnotations.initMocks(this);
}

/**
* stubbling
*/
@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);
}

/**
* stubbling
*/
@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
/**
* @author mofan 2020/12/18
*/
@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() {
// 重置 Stubbing
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() {
// 测试执行一次返回值类型是 void 的方法
Mockito.doNothing().when(list).clear();
list.clear();
Mockito.verify(list, Mockito.times(1)).clear();

// 测试执行返回值类型是 void 的方法抛出异常
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, 2, 3, 4);
*/
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);
// 第五次调用结果还是 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 -> {
// 指定 get() 方法的第一个参数是 Integer 类型,名为 index
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
/**
* @author mofan 2020/12/18
*/
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
/**
* @author mofan 2020/12/19
*/
@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
/**
* @author mofan 2020/12/19
*/
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.when(list.get(0)).thenReturn(100);

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
/**
* @author mofan 2020/12/21
*/
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);
// 放开下面这段代码,测试不会通过
// 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);
// 没有指定 Child2, 因此返回 int 类型的默认值
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;
// new 了一个自己
public static final Any ANY = new Any();

private Any() {}

// 匹配时,无论传入什么对象,都返回 true
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
/**
* @author mofan 2020/12/21
*/
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;

/**
* @author mofan 2020/12/21
*/
@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() {
/*
* 注意 Stubbing 的顺序
* 如果将第一句 Stubbing 移动到第三句,那么就会报错
*/
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() {
// 使 Collections.emptyList() 是同一个实例
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;


/**
* @author mofan 2020/12/21
*/
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)));
// not -- is? is -- not? All can!
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;

// either or
Assert.assertThat(price, either(equalTo(2.12)).or(equalTo(1.12)));
// both and
Assert.assertThat(price, both(not(equalTo(1.12))).and(not(equalTo(2.11))));
// anyOf
Assert.assertThat(price, anyOf(is(2.12), not(1.12), is(6.20)));
// allOf
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;

/**
* @author mofan 2020/12/21
*/
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;

/**
* @author mofan 2020/12/21
*/
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;

/**
* @author mofan 2020/12/21
*/
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;

/**
* @author mofan 2020/12/21
*/
public class CustomMatcherTest {
@Test
public void test1() {
// 10 > 5 ?
Assert.assertThat(10, gt(5));
// 10 < 20 ?
Assert.assertThat(10, lt(20));
// 5 < 10 < 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();

// 验证是否添加了 mofan 字符串
verify(list).add("mofan");
// 验证是否调用了一次 clear() 方法
verify(list).clear();
// 等价于
verify(list, times(1)).clear();
}

如果在上述测试代码中添加一行以下代码:

1
verify(list).add("默烦");

由于我们并没有添加字符串 “默烦” 进 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
/**
* @author mofan 2020/12/21
*/
@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");

// 验证执行 add("once") 一次
verify(list).add("once");
// 验证执行 add("two") 两次
verify(list, times(2)).add("twice");
// 验证执行 add("third") 三次
verify(list, times(3)).add("third");

// 验证没有执行 add("mofan")
verify(list, never()).add("mofan");

// atLeast / atMost
// 最少执行一次, 不足报错
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");

// 为 mock 对象创建一个 InOrder 对象
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");

// 为两个 mock 对象创建 InOrder 对象
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");

// 验证 firstList 调用了 add(),进行一次交互
verify(firstList).add("mofan");
// 验证某个交互没有执行
verify(secondList, never()).add("默烦");
// 验证某些 mock 对象没有交互过
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);

// 调用 add(), 但未进行验证
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);

// 调用 add(), 也进行验证
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));
/*
* 下面的测试不会通过因为没有对 Stubbing 进行验证
* verifyNoMoreInteractions(firstList, secondList);
*/
// 由于忽略了 firstList secondList,即使 get 方法没有 verify 也通过
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
// 验证 100 ms 后调用了 一次
verify(mock, timeout(100)).someMethod();
// 等价于
verify(mock, timeout(100).times(1)).someMethod();

// 验证 100 ms 后调用了 两次
verify(mock, timeout(100).times(2)).someMethod();

// 验证 100 ms 后至少调用了 两次
verify(mock, timeout(100).atLeast(2)).someMethod();

// verifies someMethod() within given time span using given verification mode
// useful only if you have your own custom verification modes.
verify(mock, new Timeout(100, yourOwnVerificationMode)).someMethod();

7.8 自定义验证失败信息

Assert.assertThat() 一样,Mockito.verify() 也允许自定义验证失败信息。比如:

1
2
3
4
5
// will print a custom message on verification failure
verify(mock, description("This will print on failure")).someMethod();

// will work with any verification mode
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;
/**
* @author mofan 2020/12/22
*/
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);
// argument 只有 verify 之后才有值
verify(list, times(2)).add(argument.capture());
// getValue 是最后一次的参数值
Assert.assertEquals(temp, argument.getValue().intValue());
// getAllValues() 包含所有调用的参数值
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;

/**
* @author mofan 2020/12/22
*/
@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.MockMakerorg.mockito.plugins.MemberAccessor,前者也是用于 Mock final 类和 final 方法的。

因此在已经引入了 mockito-core 3.4.0 及其以上版本的情况下,如果不想将原依赖修改为 mockito-inline,可以在 classpath 下创建相同的文件及其内容也能够成功 Mock 静态方法。

比如:

Mock静态方法插件配置

org.mockito.plugins.MockMaker 文件中的信息:

1
mock-maker-inline

org.mockito.plugins.MemberAccessor 文件中的信息:

1
member-accessor-module

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");

// mock 后,未进行 stubbing 的方法返回 null
Assert.assertNull(TestUtil.generateHello("boy"));
// 注销注册的静态 Mock
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");
// 进行 verify
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。