Mockito&PowerMockito使用

为什么要进行mock数据

平时我们在写单测时,因为需要测试一些业务类,不可避免在业务类中会访问外部存储资源,例如数据库、外部接口等。如果编写了依赖外部资源的单测,在运行时不可避免有两个问题:

  1. 启动或运行慢
  2. 外部数据不稳定,可能会导致单测运行结果与预期不符

在很多Git管理的项目中,会配置一些自动化的流水线,可能在每一次提交或MR后会全量跑一遍所有单测,一个大项目可能动辄几百几千个单测方法,如果每一个都要去访问外部资源,那么一次流程走完会浪费很多时间;其次外部数据不稳定,原来A状态的数据变成了B状态,单测可能就无法通过了,这样对单测的维护也造成了很大的困难。

鉴于上述原因,单测不应该依赖外部的数据源,我们的单测中如果需要是调用访问外部数据源的方法,最好的办法就行进行mock。

Java项目中,在这里推荐一个mock数据的神器——Mockito,以及它的升级版PowerMockito

Start

引入依赖

一般通过maven或gradle引入PowerMock依赖,此处按maven举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.4.6</version>
<scope>test</scope>
</dependency>

其中版本号可以替换,目前powermock的最新版本为2.0.7,mockito的最新版本是3.4.6

开始配置

如果需要使用MockitoPowerMockito,需要在Junit类上添加@RunWith注解,如果只用到了Mockito,可以使用MockitoJUnitRunner作为参数,但很多情况下会需要用到PowerMockito中的特性,所以可以直接使用PowerMockRunner作为参数。

1
2
3
@RunWith(PowerMockRunner.class)
public class Test {
}

InjectMock

InjectMock用来创建需要被测试的对象

1
2
3
4
5
@RunWith(PowerMockRunner.class)
public class Test {
@InjectMock
private Demo demo;
}

Mock

对于一些需要mock的方法对象,需要创建其mock代理,一般有两种方式

  1. 使用@Mock注解,用这种方式创建的对象,不需要手动注入到测试对象中
  2. 调用Mockito#mock方法,用这种方式创建的对象,需要手动注入到测试对象中

通过该种方式创建的代理对象,每个方法都会被代理,当需要mock调用时,都需要mock其方法实现,如果没有mock实现,则会返回默认值(基本类型会返回0或false,非默认类型会返回null)。

注意与下文Spy进行甄别。

1
2
3
4
5
6
7
8
@RunWith(PowerMockRunner.class)
public class Test {
@InjectMock
private Demo demo;
@Mock
private Service service;

}

Spy

Spy方式mock的对象的差别如果没有手动mock其实现,则会调用其原有的真实实现逻辑,当需要时,可以对其中一个或某个方法进行mock。

另一个差别是,使用@Spy注解的对象需要手动创建实例。

注意与Mock进行甄别。

1
2
3
4
5
6
7
8
@RunWith(PowerMockRunner.class)
public class Test {
@InjectMock
private Demo demo;
@Spy
private Service service = new Service();

}

方法mock

mock方法返回值

1
Mockito.when().thenReturn()

例如,有如下方法的返回值需要进行mock

1
2
3
4
5
class Test{
public String parseLongToString(long value){
return String.valueOf(value);
}
}

假设希望无论入参是什么值,都返回hello,world,那么可以这样:

1
Mockito.when(test.parseLongToString(Mockito.anyLong())).thenReturn("hello,world");

其中test为需要mock的对象,其中Mockito.anyLong()表示匹配所有的long类型的参数,thenReturn方法的入参为当方法被调用时,会返回该入参值。

在很多场景下,需要mock一个方法的多次执行,但是需要根据入参不同来进行mock,例如上述Test#parseLongToString方法在入参小于0时返回-1,等于0时返回0,大于0时返回1,那么可以这么写:

1
2
3
4
5
6
7
8
9
Mockito.when(test.parseLongToString(Mockito.anyLong())).thenAnswer(invocation -> {
long argument = ((long) invocation.getArgument(0));
if (argument < 0) {
return -1;
} else if (argument > 0) {
return 1;
}
return 0;
});

mock 静态方法

如果需要mock静态方法,会稍微麻烦一些,首先需要在测试类之前增加一个注解@PrepareForTest,并将需要mock的类作为参数传入,例如:

1
2
3
4
5
6
import org.powermock.core.classloader.annotations.PrepareForTest;

@RunWith(PowerMockRunner.class)
@PrepareForTest({TestUtil.class})
public class MockDemo {
}

在mock前需要先开启静态类的mock:

1
2
3
// 这两种mock方式均可,mock与spy的差别见上文
PowerMockito.mockStatic(TestUtil.class);
PowerMockito.spy(TestUtil.class);

如果需要mock返回值,与上一章节中mock普通方法类似,不过需要调用PowerMockito中的方法:

1
2
3
// 下面两种mock方式等效
PowerMockito.when(TestUtil.abs(Mockito.anyDouble())).thenReturn(10086D);
PowerMockito.when(TestUtil.class, "abs", Mockito.anyDouble()).thenReturn(10086D);

有些情况下,需要mock没有返回值的方法,此处假设TestUtil类中有一个会抛出异常的方法,例如:

1
2
3
4
5
public class TestUtil {
public static void throwException(){
throw new IllegalStateException();
}
}

此时希望这个方法在测试时不抛出异常,即希望这个方法什么都不做:

1
2
// 如果需要mock没有返回值的方法,需要用这种方式,doNothing表示该方法不做任何事情
PowerMockito.doNothing().when(TestUtil.class, "throwException");

也可以修改throwException的内容,例如抛出一个NPE:

1
2
// 如果需要mock方法实现,可以使用doAnswer修改其实现
PowerMockito.doAnswer(invocation -> {throw new NullPointerException();}).when(TestUtil.class, "throwException");

mock私有方法

私用方法mock方式是在PowerMockito中提供的,因为私有方法不能直接调用,所以使用方法名作为参数进行mock,例如:

1
PowerMockito.when(service, "increase", Mockito.anyInt()).thenReturn(888);

mock方法内部创建的对象

在有些场景下,需要mock的对象是在方法内部创建的,那么对于这个对象的mock,就不能像上文那样,直接增加一个@Mock注解就进行mock,而需要mock初始化方法。

例如有这么一个方法,其中的Service对象需要mock:

1
2
3
4
5
6
7
8
public class Demo {
public int stringToInt(String value) {

Service service = new Service();

return service.stringToInt(value);
}
}

首先需要给单元测试类增加一个@PrepareForTest注解,其参数为被测试类,这里请注意,是被测试类,而不是需要mock的类,在这个例子中,就是Demo类。

然后需要mock类初始化方法,也就是new操作,可以使用PowerMockito.whenNew方法进行mock

new操作被mock后,再mock具体的方法

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RunWith(PowerMockRunner.class)
@PrepareForTest({Demo.class})
public class MockDemo {

@InjectMocks
private Demo demo;

@Test
public void test() throws Exception {

Service service = Mockito.mock(Service.class);

// 这一步是mock对象的new操作
PowerMockito.whenNew(Service.class).withAnyArguments().thenReturn(service);
// 这一步是mock目标方法
PowerMockito.when(service.stringToInt(Mockito.anyString())).thenReturn(888);

System.out.println(demo.stringToInt("123"));

}
}

验证行为

验证方法调用次数

验证public方法

Mockito.verify方法可以用来校验方法被调用次数,例如上文提到的例子中Demo#stringToInt方法中调用了Service#stringToInt,如果需要检测Service#stringToInt是否被调用且仅被调用了一次,可以用如下代码:

1
Mockito.verify(service, Mockito.times(1)).stringToInt(Mockito.anyString());

其中Mockito.times(1)参数表示对于目标方法调用次数的预期是一次,这个方法用来提供明确的预期值。

Mockito#atLeast(int)的返回值作为入参,这个方法的返回值表示对目标方法最少次数调用的预期。

Mockito#atMost(int)的返回值作为入参,这个方法的返回值表示对目标方法最多次数调用的预期。

验证private方法

验证私有方法需要使用PowerMockito中的verifyPrivate方法,入参与Mockito.verify基本一致。差别在于验证public方法时,因为是共有方法,所以可以直接调用,但是private需要通过invoke进行调用,例如:

1
PowerMockito.verifyPrivate(service, Mockito.times(1)).invoke("increase", Mockito.anyInt());

invoke方法入参可以是参数名+参数列表,也可以是反射得到的方法对象,不过如果使用这种传参方式,就需要在之后反射调用一次该方法,否则无法判断具体要验证什么类型的入参,例如:

1
2
3
Method increaseMethod = Service.class.getDeclaredMethod("increase", int.class);
PowerMockito.verifyPrivate(service, Mockito.times(1)).invoke(increaseMethod);
increaseMethod.invoke(service,Mockito.anyInt());

验证静态方法

验证静态方法与mock静态方法类似,需要先@PrepareForTest准备需要验证的目标静态方法所在类;在验证之前,需要调用PowerMockito#mockStatic方法进行mock

等调用完目标方法后,可以调用PowerMockito#verifyStatic方法进行验证,与验证public方法不同的是,验证静态方法需要再手动调用一次这个静态方法,例子如下:

1
2
PowerMockito.verifyStatic(Service.class,Mockito.times(1));
Service.stringToInt(Mockito.anyString());

其中verifyStatic方法的第一个参数为需要验证的目标方法所在类,第二个参数为预期的调用次数。

执行完verifyStatic后,必须要调用一次需要验证次数的方法,其参数可以用精确值,也可以使用类似Mockito.anyString()之类的模糊匹配值。

验证方法请求参数

在有些情况下,不光希望验证某个方法的调用次数,也希望可以验证调用时的入参是否符合预期,那么需要拦截其入参,再进行验证,可以通过ArgumentCaptor进行拦截。

ArgumentCaptor#forClass方法可以创建一个拦截器,并调用其capture的返回值作为预期方法的入参。当方法被调用后,再获取拦截器中的值进行验证。

例子:

1
2
3
4
5
System.out.println(demo.stringToInt("123"));
ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(int.class);
Mockito.verify(service,Mockito.times(1)).increase(captor.capture());

Assert.assertEquals(1233,(int)captor.getValue());

上述例子中创建了一个ArgumentCaptor对象,并调用其capture方法传入需要拦截的方法中。这种方式将会拦截该方法的所有int类型的参数。

如果目标方法预期只会调用一次,那么可以调用ArgumentCaptor#getValue获取唯一的入参,再使用Assert进行验证是否符合预期。

如果目标方法调用超过一次,可以调用ArgumentCaptor#getAllValues方法获取所有的入参,再进行逐一验证。

如果这篇文章对你有帮助,可以请作者喝杯咖啡~