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

本文参考:汪文君Mockito实战视频

本文涉及的代码:mofan212/mockito

Mockito 官网:Mockito

Mockito 英文文档:Mockito-Doc-EN

本文依赖 JDK 17 与 Junit5

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
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>

注意: 不建议直接导入 mockito-all 依赖,这样导入可能会出现一些错误,建议导入 mockito-core 包。

项目结构与代码准备

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

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
/**
* @author mofan 2020/12/18
*/
@ExtendWith(MockitoExtension.class)
public class AccountLoginControllerTest {
private AccountDao accountDao;
private HttpServletRequest request;
private AccountLoginController accountLoginController;

@BeforeEach
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(ArgumentMatchers.anyString(), ArgumentMatchers.anyString())).thenReturn(account);
Assertions.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(ArgumentMatchers.anyString(), ArgumentMatchers.anyString())).thenReturn(null);
// 返回 /login
Assertions.assertEquals(accountLoginController.login(request), "/login");
}


@Test
public void testLogin505() {
Mockito.when(request.getParameter("username")).thenReturn("404");
Mockito.when(request.getParameter("password")).thenReturn("500");
// 指定 findAccount() 方法返回 null
Mockito.when(accountDao.findAccount(ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenThrow(UnsupportedOperationException.class);
// 返回 /login
Assertions.assertEquals(accountLoginController.login(request), "/505");
}
}

运行上述测试方法,无论是单个运行,还是组合运行都能够测试通过。

3. How to mock

3.1 几种不同的 Mock 方式

使用静态方法 Mockito.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
import org.mockito.junit.jupiter.MockitoExtension;

/**
* @author mofan 2020/12/18
*/
public class MockByStaticMethodTest {
@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");

@Mock 注解

使用这种方式,需要在测试之前执行 MockitoAnnotations.openMocks(this);,然后在测试类中声明一个成员变量(这个成员变量就是 mock 的对象),并在这个成员变量上使用 @Mock 注解。

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
/**
* @author mofan 2020/12/18
*/
public class MockByAnnotationTest {

// @Mock(answer = Answers.RETURNS_SMART_NULLS)
@Mock
private AccountDao accountDao;

private AutoCloseable closeable;

@BeforeEach
public void init() {
closeable = MockitoAnnotations.openMocks(this);
}

@AfterEach
public void destroy() throws Exception {
closeable.close();
}

@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 初始化工作后才能使用。初始化方式有两种:

  • 测试方法执行前执行 MockitoAnnotations.openMocks(this);

  • 在测试类上使用 @ExtendWith(MockitoExtension.class),如果使用 Junit4,则替换为 @RunWith(MockitoJUnitRunner.class)

因此下述代码将与输出上述代码一样的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
@ExtendWith(MockitoExtension.class)
public class MockByAnnotationTest {

@Mock
private AccountDao accountDao;

@Test
public void testMock() {
Account account = accountDao.findAccount("x", "x");
// null
System.out.println(account);
}
}

通过 @Rule

@Rule 是 Junit4 中的注解,Junit5 中移除了该注解。

使用这种方式,需要在测试类中添加:

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 注解:

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

@ExtendWith(MockitoExtension.class)MockitoAnnotations.openMocks(this);

作用一:

@ExtendWith(MockitoExtension.class)MockitoAnnotations.openMocks(this); 都可以为单元测试提供框架使用的自动验证。

在编写单元测试时,若在 mock 数据有语法或者书写错误,框架使用的自动验证会在单元测试运行时报告出来。比如:

  • 使用 Mockito.when() 方法时,后面没有跟 thenReturn()thenThrow() 等方法
  • 使用 doReturn() 后再使用 when() 时,后面没有跟方法的调用

作用二:

@ExtendWith(MockitoExtension.class)MockitoAnnotations.openMocks(this); 都可以为单元测试提供 mock 初始化工作。

在使用 @Mock@Spy@InjectMocks 等注解时, 必须先初始化才能使用。

如果在单元测试类中使用了@ExtendWith(SpringExtension.class),将无法再使用@ExtendWith(MockitoExtension.class),需要使用 MockitoAnnotations.openMocks(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;

@BeforeEach
public void init() {
MockitoAnnotations.openMocks(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
27
28
29
30
31
32
33
/**
* @author mofan 2020/12/18
*/
public class DeepMockTest {
@Mock
private UserService userService;

@Mock
private User user;

private AutoCloseable closeable;

@BeforeEach
public void init() {
closeable = MockitoAnnotations.openMocks(this);
}

@AfterEach
public void destroy() throws Exception {
closeable.close();
}

/**
* 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
public class DeepMockTest {

@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private UserService userService;

// --snip--

/**
* 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
*/
@ExtendWith(MockitoExtension.class)
public class StubbingTest {
private ArrayList<String> list;

@Before
@SuppressWarnings("unchecked")
public void init() {
this.list = Mockito.mock(ArrayList.class);
}

@AfterEach
@SuppressWarnings("unchecked")
public void destroy() {
// 重置 Stubbing
Mockito.reset(list);
}

@Test
public void howToUseStubbing() {
Mockito.when(list.get(0)).thenReturn("first");
MatcherAssert.assertThat(list.get(0), CoreMatchers.equalTo("first"));

Mockito.when(list.get(Mockito.anyInt())).thenThrow(new RuntimeException());

try {
String s = list.get(0);
Assertions.fail();
} catch (Exception e) {
// 断言抛出异常
MatcherAssert.assertThat(e, CoreMatchers.instanceOf(RuntimeException.class));
}
}
}

4.2 void 类型的方法

并不是所有方法都有返回值,方法的返回值类型是 void 也是很常见的,那么返回值是 void 类型的方法怎么使用 Stubbing 呢?

在这节,主要讲述返回值类型是 void 的方法,然后进行以下测试:

  • 测试成功执行一次
  • 测试执行时抛出异常
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();
Assertions.fail();
} catch (Exception e) {
MatcherAssert.assertThat(e, CoreMatchers.instanceOf(RuntimeException.class));
}
}

4.3 doReturn

前面的示例中是先调用 when() 再调用 thenReturn(),其实还可以先调用 doReturn() 再调用 when(),这两种方式写法有些不同,但是最终效果是一样的。

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

MatcherAssert.assertThat(list.get(0), CoreMatchers.equalTo("first"));
Assertions.assertEquals(list.get(1), "second");
}

4.4 迭代式 Stubbing

如果要实现第一次调用一个方法返回 a,第二次调用返回 b,第三次调用返回 c,这应该怎么做呢?

可以使用迭代式 Stubbing,这种方式有两种实现:

  1. 调用 thenReturn() 方法时,指定多个参数,不同调用次数得到的返回值与参数的顺序一致
  2. 多次调用 thenReturn() 方法,不同调用次数得到的返回值与调用 thenReturn() 方法的顺序一致
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);

Assertions.assertEquals(list.size(), 1);
Assertions.assertEquals(list.size(), 2);
Assertions.assertEquals(list.size(), 3);
Assertions.assertEquals(list.size(), 4);
// 第五次调用结果还是 4
Assertions.assertEquals(list.size(), 4);
}

4.5 thenAnswer

如果在测试的方法的返回值存在一定的逻辑处理关系,应该怎么测试呢?

ArrayList<String> 而言,get(n) 得到的结果总是 n * 10,应该怎么测试?

可以用到 thenAnswer() 方法。

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.getArgument(0, Integer.class);
return String.valueOf(index * 10);
});

for (int i = 0; i < 10; i++) {
int num = (int)(Math.random() * 100) + 1;
Assertions.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");
Assertions.assertEquals(service.getS(), "mofan");

Mockito.when(service.getI()).thenCallRealMethod();
Assertions.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
*/
@ExtendWith(MockitoExtension.class)
public class SpyingTest {

@Test
public void testSpy() {
List<String> realList = new ArrayList<>();
List<String> list = Mockito.spy(realList);

list.add("mofan");
list.add("默烦");

Assertions.assertEquals(list.get(0), "mofan");
Assertions.assertEquals(list.get(1), "默烦");
Assertions.assertEquals(list.size(), 2);

Mockito.when(list.size()).thenReturn(100);
Assertions.assertTrue(list.size() != 2);
Assertions.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
25
26
27
28
29
30
31
/**
* @author mofan 2020/12/19
*/
public class SpyAnnotationTest {
@Spy
List<String> list = new ArrayList<>();

private AutoCloseable closeable;

@BeforeEach
public void init() {
closeable = MockitoAnnotations.openMocks(this);
}

@AfterEach
public void destroy() throws Exception {
closeable.close();
}

@Test
public void testSpyByAnnotation() {
list.add("one");
list.add("two");

Assertions.assertEquals(list.get(0), "one");
Assertions.assertEquals(list.get(1), "two");

Mockito.when(list.size()).thenReturn(100);
Assertions.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);
Assertions.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();

Assertions.assertTrue(isSpy);
Assertions.assertTrue(notMock);
Assertions.assertTrue(isMock);
Assertions.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("默烦");

Assertions.assertEquals(list.get(0), "mofan");
Assertions.assertEquals(list.get(1), "默烦");
Assertions.assertNull(list.get(2));
// 还可以验证一下
Mockito.verify(list).get(0);
// 放开下面这段代码,测试不会通过
// Mockito.verify(list).get(0);
}
}

6.2 isA() 与 any() 的使用

环境准备

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

Assertions.assertEquals(result_1, 200);
Assertions.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());

Assertions.assertEquals(result_1, 200);
// 没有指定 Child2, 因此返回 int 类型的默认值
Assertions.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
4
public static <T> T isA(Class<T> type) {
reportMatcher(new InstanceOf(type));
return defaultValue(type);
}

测试 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);

Assertions.assertEquals(foo.function(new Child1()), 100);
Assertions.assertNotEquals(foo.function(new Child2()), 100);
}

上述代码可以测试通过,感觉和 isA() 好像区别很大? ⁉️ ​看看它的源码!

Mockito.any() 方法的源码:

1
2
3
4
public static <T> T any(Class<T> type) {
reportMatcher(new InstanceOf(type, "<any " + type.getCanonicalName() + ">"));
return defaultValue(type);
}

reportMatcher() 方法中使用了 Any.ANY,来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Any implements ArgumentMatcher<Object>, Serializable {

// new 了一个自己
public static final Any ANY = new Any();

@Override
public boolean matches(Object actual) {
// 匹配时,无论传入什么对象,都返回 true
return true;
}

@Override
public String toString() {
return "<any>";
}

@Override
public Class<?> type() {
return Object.class;
}
}

从源码中很容易明白,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
*/
@ExtendWith(MockitoExtension.class)
public class WildcardArgumentMatcherTest {
@Mock
private SimpleService simpleService;

@AfterEach
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(), "默烦");
Assertions.assertEquals(result_1, 100);
int result_2 = simpleService.method1(888, "默烦", Collections.emptySet(), "mofan");
Assertions.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(), "默烦");

Assertions.assertEquals(result_1, 100);
Assertions.assertEquals(result_2, 200);
Assertions.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

前文中,使用断言 Assertions 进行验证时,使用的都是 assertEquals()assertTrue() 等类似的方法,其实不建议这样做,这种写法有很大的局限性。如果满足期望值中的某一个就测试通过,又或者不满足所有期望值才测试通过,这些情况该怎么验证呢?

因此,建议使用 org.hamcrest.MatcherAssert.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;

MatcherAssert.assertThat(i, equalTo(10));
MatcherAssert.assertThat(i, not(equalTo(20)));
MatcherAssert.assertThat(i, is(10));
MatcherAssert.assertThat(i, not(is(20)));
// not -- is? is -- not? All can!
MatcherAssert.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
MatcherAssert.assertThat(price, either(equalTo(2.12)).or(equalTo(1.12)));
// both and
MatcherAssert.assertThat(price, both(not(equalTo(1.12))).and(not(equalTo(2.11))));
// anyOf
MatcherAssert.assertThat(price, anyOf(is(2.12), not(1.12), is(6.20)));
// allOf
MatcherAssert.assertThat(price, allOf(is(2.12), not(is(1.12)), not(2.11)));

MatcherAssert.assertThat(Stream.of(1, 2, 3).allMatch(i -> i > 0), equalTo(true));
}

使用 MatcherAssert.assertThat() 甚至还可以指定校验错误原因:

1
2
3
4
5
6
7
8
@Test
public void test3() {
double price = 2.12;

// 下述验证将不会通过
MatcherAssert.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>

在 Junit4 中运行时遇到的问题

在初次运行 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 的类,而这些类可能正在被使用,或者排除 Junit 中依赖的 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
27
28
29
30
<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-core</artifactId>
<version>${mockito.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
</dependencies>

流式断言神器 - AssertJ

最近接触到流式断言 AssertJ,使用体验非常好,源码仓库和文档参考以下链接:

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 ?
MatcherAssert.assertThat(10, gt(5));
// 10 < 20 ?
MatcherAssert.assertThat(10, lt(20));
// 5 < 10 < 20 ?
MatcherAssert.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 验证某些行为

基本使用

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
*/
@ExtendWith(MockitoExtension.class)
public class VerifyArgumentMatcherTest {
@Mock
private SimpleService simpleService;

@AfterEach
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 verifyNoInteractions

还可以验证某个或者某些 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 对象没有交互过
verifyNoInteractions(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);

MatcherAssert.assertThat(firstList.get(0), CoreMatchers.equalTo(10));
MatcherAssert.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 自定义验证失败信息

MatcherAssert.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();

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, rawtypes")
public void testCaptureArgument() {
List<String> list = List.of("1", "2");
List<String> mockedList = mock(ArrayList.class);
ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
mockedList.addAll(list);
// 参数的捕获
verify(mockedList).addAll(argument.capture());
// 验证捕获的参数
Assertions.assertEquals(2, argument.getValue().size());
Assertions.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
16
@Test
@SuppressWarnings("unchecked")
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 是最后一次的参数值
Assertions.assertEquals(temp, argument.getValue().intValue());
// getAllValues() 包含所有调用的参数值
Assertions.assertTrue(argument.getAllValues().contains(temp));
Assertions.assertTrue(argument.getAllValues().contains(1));
}

根据官方文档: 建议使用没有 Stubbing 的 ArgumentCaptor 来验证,使用含有 Stubbing 的 ArgumentCaptor 会降低测试代码的可读性,因为 captor 是在断言代码块之外创建的。另一个好处是它可以降低本地化的缺点,因为如果 Stubbing 函数没有被调用,那么参数就不会被捕获。

使用 ArgumentCaptor 在以下的情况下更合适:

  • 自定义不能被重用的参数匹配器
  • 仅需要断言参数值

8.2 一些注解

前面也介绍了类似于 @Mock 这样的注解,需要明白这些注解并不是一添加就生效,要想让这些注解生效,需要:

  • 在测试类上使用:@ExtendWith(MockitoExtension.class)
  • @BeforeEach 标记的方法中调用:MockitoAnnotations.openMocks(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
25
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

/**
* @author mofan 2020/12/22
*/
@ExtendWith(MockitoExtension.class)
public class ArgumentCaptorAnnotationTest {

@Captor
ArgumentCaptor<List<String>> captor;

@Test
@SuppressWarnings("unchecked")
public void testCaptureArgument() {
List<String> list = List.of("1", "2");
List<String> mockedList = mock(ArrayList.class);
mockedList.addAll(list);
// 参数的捕获
verify(mockedList).addAll(captor.capture());
// 验证捕获的参数
Assertions.assertEquals(2, captor.getValue().size());
Assertions.assertEquals(list, captor.getValue());
}
}

@InjectMocks

@InjectMocks:创建一个实例,其余用 @Mock 注解创建的 mock 对象将被注入到该实例中。

类似 Spring 中的 @Autowried,具体使用方式参考:@InjectMocks

@InjectMocks 不支持将 mock 对象注入到 Spy 对象中,如果要实现这样的功能,需要在构造 Spy 对象时传入 mock 对象。

@MockBean@SpyBean

它们是 SpringBoot 中的注解,相当于 @Mock@Spy,并且被标记的对象会自动添加到 Spring 容器中。

9. Integration with SpringBoot

9.1 整合 Mockito

在前文中已经介绍了 Mockito 的基本使用,但是在实际开发过程中面对的代码和业务更复杂,不会使用上述 Demo 级别的代码进行测试,并且会选择与 Spring 或 SpringBoot 进行整合。

在实际开发中,一个类往往会依赖多个类,这个时候如果需要测试就会很麻烦。

为了增加代码的简洁性,选择使用 @Mock 注解来 mock 对象。所以别忘记使用 ExtendWith 注解或者在 @BeforeEach 注解标记的方法内添加 MockitoAnnotations.openMocks(this);

在对某个类 A 进行测试时,A 中依赖了 B,可以 mock 一个 B 将其注入到 A 中,这时需要使用到 @InjectMocks@Mock 注解。真正被测试的类使用 @InjectMocks 注解,被依赖的类使用 @Mock 进行 mock,这样就可以将 mock 出的对象注入到被 @InjectMocks 注解标记的类。

如果在测试中还使用了 Spring 的 @Autowired 注解来进行依赖注入,为了防止注入失败而导致报错,使用 Junit4 的情况下需要在测试类上添加 @ExtendWith(SpringExtension.class) 注解,使用 Junit5 时则是在测试类上添加 @ExtendWith(SpringExtension.class) 注解。

@InjectMocks 与 @Mock 的选择

参考链接:mock测试及jacoco覆盖率

真正需要测试的类,要用 @InjectMocks,而不是 @Mock(更不能是 @Autowired),原因如下:

原因 1:@Autowired 是 Spring 的注解,在 mock 环境下,根本就没有 Spring 上下文,当然会注入失败。

原因 2:也不能是 @Mock@Mock 表示该注入的对象是“虚构”的假对象,里面的方法代码根本不会真正运行,统一返回空对象 null,即:被 @Mock 修饰的对象,在该测试类中,其具体的代码永远无法覆盖到!这也就是失败了单元测试的意义。而 @InjectMocks 修饰的对象,被测试的方法,才会真正进入执行。

另外,测试服务时,被 mock 注入的类,应该是具体的服务实现类,即:xxxServiceImpl,而不是服务接口,在 mock 环境中接口是无法实例化的。

9.2 挽救不规范的单元测试

在工程项目中, 一般都会使用 SpringBoot 作为基本依赖,将对象交由 Spring 管理,常常使用 @Autowird 注解来完成自动注入。

标准的单元测试中,不应该启动 Spring,此时就需要使用 @InjectMocks 完成依赖注入。@InjectMocks 会将带有 @Spy@Mock 注解的对象尝试注入到被测试的目标类中,这在【9.1 整合 Mockito】中已经讲过,不再赘述。

关于单元测试与集成测试,有这样两句话:

  1. Usually when you are unit testing, you shouldn’t initialize Spring context. So remove Autowiring.
  2. Usually when you do integration testing, you should use real dependencies. So remove mocking.

简单来说,单元测试中不应该初始化 Spring 容器,因此不要使用 @Autowird,而是使用 Mock;在集成测试中则是相反。

场景

现有如下代码:

1
2
3
4
5
6
@Repository
public class UserDao {
public void doSomething() {
throw new UnsupportedOperationException();
}
}
1
2
3
4
5
6
7
8
9
@Component
public class UserService {
@Autowired
private UserDao userDao;

public void doSomething() {
userDao.doSomething();
}
}

然后需要对 UserService 进行单元测试,但这是一个不标准的单元测试,使用了 @Autowired 注解:

1
2
3
4
5
6
7
8
9
10
11
12
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class UserServiceTest {
@Autowired
private UserService userService;

@Test
public void testDoSomething() {
// todo
}

}

根据前面的讲解,可以想到如下方式进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class UserServiceTest {
@Autowired
@InjectMocks
private UserService userService;

@Mock
private UserDao userDao;

@Test
public void testDoSomething() {
MockitoAnnotations.openMocks(this);
// todo
}

}

但采用这种方式会影响到原本测试方法,因为最开始采用 Spring 的方式管理对象,那么 userService 中注入的 userDao 对象是由 Spring 管理的对象,而在使用 @MockuserDao 注入 userService 后,userDao 则是由 Mockito Mock 出的对象。原先按照集成测试方式进行测试的测试方法则会由于没有对 userDao 进行 Stubbing 而报错,那么有没有什么方法挽救下,并且还能够使用 Mockito 对 UserDao 进行 Mock?

使用 Spring 的 ReflectionTestUtils 工具类向 userService 中注入非 public 的字段即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class UserServiceTest {
@Autowired
private UserService userService;

@Test
public void testDoSomething() {
UserDao userDaoMock = Mockito.mock(UserDao.class);
Mockito.doNothing().when(userDaoMock).doSomething();
// 使用 Spring 的测试工具类注入非 public 的字段
ReflectionTestUtils.setField(userService, "userDao", userDaoMock);

try {
userService.doSomething();
} catch (Exception e) {
Assertions.fail();
}
}

}

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 4.3.1 及其以上版本的情况下,如果不想将原依赖修改为 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");
Assertions.assertEquals(TestUtil.helloWorld(), "Hello, Mofan");

// mock 后,未进行 stubbing 的方法返回 null
Assertions.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");
Assertions.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() {
Assertions.assertEquals(TestUtil.generateHello("boy"), "Hello, boy");
try (MockedStatic<TestUtil> mockStatic = Mockito.mockStatic(TestUtil.class)) {
mockStatic.when(() -> TestUtil.generateHello("boy")).thenReturn("Hello, girl");
Assertions.assertEquals(TestUtil.generateHello("boy"), "Hello, girl");
}
Assertions.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。