Mock与Mockito

Mock

在协同开发过程中,我们经常会遇到这样的场景。我们开发了一个类,这个类的功能是依赖于其他服务的接口,或者第三方的功能模块。但是由于是协同开发,可能其他服务的接口此时还没有实现,而我们需要对实现的类进行单元测试,那可能需要等待其他服务的实现。或者是借助的第三方功能模块非常复杂,每次测试都需要比较长的时间,此时单元测试与开发之间体验就会非常割裂。

Mock就是用来解决上面的问题的。Mock意为模拟,借助Mock我们可以模拟出所需要的对象,并且可以定义这个对象的行为。Mock可以解决如下问题:

  • 解决依赖问题:在测试一个接口或者功能模块的时候,如果这个接口或者功能模块依赖其他接口或者其他模块,且所依赖的接口或功能模块未开发完毕,那么我们可以使用Mock模拟被依赖的接口,完成目标接口的测试

  • 模拟复杂业务的接口:依赖一个非常复杂的业务或者第三方接口,可以直接使用Mock来模拟这个复杂的业务接口,定义它能够返回正确的结果

  • 单独测试:在对某个类进行测试的时候,可能会涉及到一些RPC的调用,数据库,缓存操作等,这些外部的依赖我们可以将其模拟Mock掉,达到单独测试某个类的效果

  • 前后端联调:进行前后端分离编程的时候,前端页面开发需要调用后台的接口,但是后台接口还没有开发完成,那么完全可以借助Mock来模拟后台这个接口返回想要的数据

Mockito

简介

Mocito是一个功能强大的Java测试框架。通过Mockito可以创建Mock对象,排除外部依赖对被测试类的干扰。Mocito可以模拟对象,模拟方法的返回值,模拟抛出异常等等,同时也会记录这些模拟方法的调用次数、调用顺序等,从而进行校验,断言程序是否按照我们期望的效果运行。

在Java中主流的Mock测试工具有Mockito、JMock、EasyMock。其中,Spring Boot默认的测试框架就是Mockito。

在开始使用Mockito之前,需要介绍几个简单的相关概念:

  • Mock对象:在调试期间用来作为真实对象的替代,是被测试对象所依赖的对象
  • Mock测试:在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试
  • Stub:打桩,为Mock对象的方法指定返回值(可抛出异常),即人为定义Mock对象的行为
  • Verify:行为验证,验证指定方法调用情况(是否被调用,调用次数等)
  • Assert:断言,判断输出效果是否与我们期望一致

Getting Started

为了使用Mocito,我们需要在项目中引入相关依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--Mocito相关依赖-->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.8.0</version>
<scope>test</scope>
</dependency>

<!--单元测试-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>

由于SpringBoot默认使用Mockito作为测试框架,因此如果我们正在开发的是SpringBoot项目,那么可以通过引入下面的starter来引入依赖:

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

下面简单描述当前的场景,我们现在已经完成了下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyService {

private OtherService otherService;

public MyService(OtherService otherService) {
this.otherService = otherService;
}

public Integer plus(Integer integer) {
return 1 + otherService.complexFunc(integer);
}
}

可以看到在我们的Service,实现的核心方法plus完成的是+1操作。但是它还依赖一个其他的Service,这个Service可能会进行很复杂的运算complexFunc。现在我们需要验证的就是这个+1操作能否正确完成,而不需要验证复杂运算是否能够完成。(实际上我们根本不需要知道复杂运算的逻辑)为了测试,我们可以写如下的单元测试代码(这里使用静态引入方便后续的调用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.mockito;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class MyServiceTest {

@Test
void plus() {
// 需要测试MyService MyService依赖OtherService

// mock一个OtherService
OtherService mock = mock(OtherService.class);
// 进行stub 定义mock出来的otherService对象的行为
when(mock.complexFunc(any())).thenReturn(1);

MyService myService = new MyService(mock);
Integer res = myService.plus(3);

assertEquals(2, res);
}
}

在单元测试代码中,我们首先将外部依赖OtherService对象Mock掉,生成了一个mock对象。之后进行stub,人为定义了复杂运算complexFunc()的行为,也就是这一行代码:

1
when(mock.complexFunc(any())).thenReturn(1);

这行代码的含义是,当mock对象调用到complexFunc()方法的时候,无论接收什么样的输入即any(),都返回1。有了这个保障,我们就可以执行需要测试的plus方法,由于无论传入任何值,复杂运算返回都是1,因此如果我们的plus方法正确,那么返回的res就必然是2,于是我们就可以这样断言,也就是代码中的最后一行。之后我们运行单元测试,就会显示测试通过。这就显示我们的plus方法中的核心+1确实是正常工作的,至于复杂逻辑,由于我们mock掉了,它能否正常运行我们是没有测试的,不过这也并不是我们需要关心的事情,我们这里需要测试的就是核心的+1能否正常运行。(这里也可以选择运行run with Coverage),这样会同时显示测试代码覆盖率等。

更多功能

when then

在上面的demo中,我们已经展示了stub的一种方式,即使用when().thenReturn()来确定Mock对象的方法调用返回值。实际上类似的方式还有下面一些:

  • thenThrow(Throwable t):抛出异常
  • thenAnswer(Answer<?> answer):对方法返回进行拦截处理
  • thenCallRealMethod():调用方法的真实实现

下面我们将Mock一个List对象,举例各种方法的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final ArrayList mockList = mock(ArrayList.class);

// 设置方法调用返回值
when(mockList.add("me")).thenReturn(true);

// 设置方法调用抛出异常
when(mockList.get(0)).thenThrow(new RuntimeException());

// 打桩 无返回值的方法
doNothing().when(mockList).clear();

// 对方法返回进行拦截处理
Answer<String> answer = new Answer<String>() {
@Override
public String answer(InvocationOnMock invocationOnMock) throws Throwable {
List mock = (List) invocationOnMock.getMock();
return "mock.size result => " + mock.size();
}
};
when(mockList.get(1)).thenAnswer(answer);

如果Mock对象的某个方法没有被打桩的话,那么调用该方法会返回默认值。

在较低版本的Mockito中,是不允许对static和final方法进行stub,不过在3.4.0版本之后可以Mock静态方法和final方法等,相关依赖在mockito-inline中。

行为与执行顺序验证

在创建了Mock对象之后,它就会记住所有的交互,于是我们就可以选择性的记录我们感兴趣的交互,包括调用了多少次Mock对象的方法,不同方法之间的执行顺序等。

行为验证:

1
2
3
4
5
6
7
8
9
10
11
12
// 验证方法被调用了一次
verify(mock).complexFunc(any());
// 验证方法被调用了至少2次
verify(mock, atLeast(2)).complexFunc(any());
// 验证方法被调用了最多3次
verify(mock,atMost(3)).complexFunc(1);
// 验证方法没有被调用
verify(mock,never()).complexFunc(2);
// 指定方法调用超时时间
verify(mock, timeout(100)).complexFunc(1);
// 指定时间内需要完成的次数
verify(mock,timeout(200).atLeastOnce()).complexFunc(any());

执行顺序验证,需要从Mock对象中创建InOrder对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 验证同一个对象多个方法的执行顺序
final List mockList = mock(List.class);
mockList.add("first");
mockList.add("second");
final InOrder inOrder = inOrder(mockList);
inOrder.verify(mockList).add("first");
inOrder.verify(mockList).add("second");

// 验证多个对象多个方法的执行顺序
final List mockList1 = mock(List.class);
final List mockList2 = mock(List.class);
mockList1.get(0);
mockList1.get(1);
mockList2.get(0);
mockList1.get(2);
mockList2.get(1);
final InOrder inOrder1 = inOrder(mockList1, mockList2);
inOrder1.verify(mockList1).get(0);
inOrder1.verify(mockList1).get(2);
inOrder1.verify(mockList2).get(1);

Spy监控

我们可以为真实的依赖对象创建一个Spy对象。这个对象与上面的Mock对象存在一定的区别。

Mock对象是对真实对象的完全Mock,我们创建的Mock对象与真实对象无关,我们可以对Mock对象的方法进行打桩,人为指定对应的返回值。而没有打桩的方法,则是默认的返回值。我们可以基于接口、实现类创建Mock对象。

Spy对象则是对真实对象的部分Mock,它是基于真实对象的。我们同样可以对对应的方法进行打桩。但是没有打桩的方法,走的是真实对象的实现。因此我们只能基于实现类来创建Spy对象,因为否则在调用没有打桩的方法时走真实实现的时候会出现异常。

1
2
3
4
5
6
7
8
9
10
LinkedList<Object> linkedList = new LinkedList<>();
LinkedList<Object> spy = spy(linkedList);

spy.add("one");
spy.add("two");

// 对get(0)打桩
when(spy.get(0)).thenReturn("1_stub");
System.out.println(spy.get(0)); // stub实现:1_stub
System.out.println(spy.get(1)); // 真实实现:two

注解简化

在前面的介绍中,我们都是使用mock()spy()方法来进行Mock对象或Spy对象的创建。事实上我们可以通过相关注解来管理Mock对象的创建。我们可以将外部依赖对象放在成员变量中,然后使用@Mock或者@Spy进行修饰,这就表示我们需要将这个对象创建为Mock对象或Spy对象。注解的使用需要首先开启,即第一步执行下面的代码:

1
MockitoAnnotations.openMocks(this);

由于每个单元测试方法是单独执行的,并且都需要开启注解,所以我们可以将注解的开启放在所有测试方法的前面,即实现一个setUp方法,其中完成注解功能开启。以下是使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AnnotationTest {

@Mock
private OtherService otherService;

@BeforeEach
void setUp(){
MockitoAnnotations.openMocks(this);
}

@Test
void testMyService(){
when(otherService.complexFunc(1)).thenReturn(2); // stub
System.out.println(otherService.complexFunc(1)); // 2
System.out.println(otherService.complexFunc(2)); // 0(默认值)
}
}

参考文章

  1. Mockito (Mockito 5.1.1 API) (javadoc.io)
  2. 手把手教你Mockito的使用 - 掘金 (juejin.cn)

Mock与Mockito
http://example.com/2023/02/25/Mock与Mockito/
作者
EverNorif
发布于
2023年2月25日
许可协议