为什么要进行mock数据
平时我们在写单测时,因为需要测试一些业务类,不可避免在业务类中会访问外部存储资源,例如数据库、外部接口等。如果编写了依赖外部资源的单测,在运行时不可避免有两个问题:
- 启动或运行慢
- 外部数据不稳定,可能会导致单测运行结果与预期不符
在很多Git管理的项目中,会配置一些自动化的流水线,可能在每一次提交或MR后会全量跑一遍所有单测,一个大项目可能动辄几百几千个单测方法,如果每一个都要去访问外部资源,那么一次流程走完会浪费很多时间;其次外部数据不稳定,原来A状态的数据变成了B状态,单测可能就无法通过了,这样对单测的维护也造成了很大的困难。
鉴于上述原因,单测不应该依赖外部的数据源,我们的单测中如果需要是调用访问外部数据源的方法,最好的办法就行进行mock。
Java项目中,在这里推荐一个mock数据的神器——Mockito
,以及它的升级版PowerMockito
。
Start
引入依赖
一般通过maven或gradle引入PowerMock
依赖,此处按maven举例
1 | <dependency> |
其中版本号可以替换,目前powermock的最新版本为2.0.7,mockito的最新版本是3.4.6
开始配置
如果需要使用Mockito
或PowerMockito
,需要在Junit类上添加@RunWith
注解,如果只用到了Mockito
,可以使用MockitoJUnitRunner
作为参数,但很多情况下会需要用到PowerMockito
中的特性,所以可以直接使用PowerMockRunner
作为参数。
1 |
|
InjectMock
InjectMock
用来创建需要被测试的对象
1 |
|
Mock
对于一些需要mock的方法对象,需要创建其mock代理,一般有两种方式
- 使用@Mock注解,用这种方式创建的对象,不需要手动注入到测试对象中
- 调用
Mockito#mock
方法,用这种方式创建的对象,需要手动注入到测试对象中
通过该种方式创建的代理对象,每个方法都会被代理,当需要mock调用时,都需要mock其方法实现,如果没有mock实现,则会返回默认值(基本类型会返回0或false,非默认类型会返回null)。
注意与下文Spy
进行甄别。
1 |
|
Spy
Spy
方式mock的对象的差别如果没有手动mock其实现,则会调用其原有的真实实现逻辑,当需要时,可以对其中一个或某个方法进行mock。
另一个差别是,使用@Spy
注解的对象需要手动创建实例。
注意与Mock
进行甄别。
1 |
|
方法mock
mock方法返回值
1 | Mockito.when().thenReturn() |
例如,有如下方法的返回值需要进行mock
1 | class Test{ |
假设希望无论入参是什么值,都返回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 | Mockito.when(test.parseLongToString(Mockito.anyLong())).thenAnswer(invocation -> { |
mock 静态方法
如果需要mock静态方法,会稍微麻烦一些,首先需要在测试类之前增加一个注解@PrepareForTest
,并将需要mock的类作为参数传入,例如:
1 | import org.powermock.core.classloader.annotations.PrepareForTest; |
在mock前需要先开启静态类的mock:
1 | // 这两种mock方式均可,mock与spy的差别见上文 |
如果需要mock返回值,与上一章节中mock普通方法类似,不过需要调用PowerMockito中的方法:
1 | // 下面两种mock方式等效 |
有些情况下,需要mock没有返回值的方法,此处假设TestUtil类中有一个会抛出异常的方法,例如:
1 | public class TestUtil { |
此时希望这个方法在测试时不抛出异常,即希望这个方法什么都不做:
1 | // 如果需要mock没有返回值的方法,需要用这种方式,doNothing表示该方法不做任何事情 |
也可以修改throwException的内容,例如抛出一个NPE:
1 | // 如果需要mock方法实现,可以使用doAnswer修改其实现 |
mock私有方法
私用方法mock方式是在PowerMockito中提供的,因为私有方法不能直接调用,所以使用方法名作为参数进行mock,例如:
1 | PowerMockito.when(service, "increase", Mockito.anyInt()).thenReturn(888); |
mock方法内部创建的对象
在有些场景下,需要mock的对象是在方法内部创建的,那么对于这个对象的mock,就不能像上文那样,直接增加一个@Mock
注解就进行mock,而需要mock初始化方法。
例如有这么一个方法,其中的Service对象需要mock:
1 | public class Demo { |
首先需要给单元测试类增加一个@PrepareForTest
注解,其参数为被测试类,这里请注意,是被测试类,而不是需要mock的类,在这个例子中,就是Demo类。
然后需要mock类初始化方法,也就是new操作,可以使用PowerMockito.whenNew
方法进行mock
new操作被mock后,再mock具体的方法
例子如下:
1 |
|
验证行为
验证方法调用次数
验证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 | Method increaseMethod = Service.class.getDeclaredMethod("increase", int.class); |
验证静态方法
验证静态方法与mock静态方法类似,需要先@PrepareForTest
准备需要验证的目标静态方法所在类;在验证之前,需要调用PowerMockito#mockStatic
方法进行mock
等调用完目标方法后,可以调用PowerMockito#verifyStatic
方法进行验证,与验证public方法不同的是,验证静态方法需要再手动调用一次这个静态方法,例子如下:
1 | PowerMockito.verifyStatic(Service.class,Mockito.times(1)); |
其中verifyStatic
方法的第一个参数为需要验证的目标方法所在类,第二个参数为预期的调用次数。
执行完verifyStatic
后,必须要调用一次需要验证次数的方法,其参数可以用精确值,也可以使用类似Mockito.anyString()
之类的模糊匹配值。
验证方法请求参数
在有些情况下,不光希望验证某个方法的调用次数,也希望可以验证调用时的入参是否符合预期,那么需要拦截其入参,再进行验证,可以通过ArgumentCaptor
进行拦截。
ArgumentCaptor#forClass
方法可以创建一个拦截器,并调用其capture
的返回值作为预期方法的入参。当方法被调用后,再获取拦截器中的值进行验证。
例子:
1 | System.out.println(demo.stringToInt("123")); |
上述例子中创建了一个ArgumentCaptor对象,并调用其capture方法传入需要拦截的方法中。这种方式将会拦截该方法的所有int类型的参数。
如果目标方法预期只会调用一次,那么可以调用ArgumentCaptor#getValue
获取唯一的入参,再使用Assert
进行验证是否符合预期。
如果目标方法调用超过一次,可以调用ArgumentCaptor#getAllValues
方法获取所有的入参,再进行逐一验证。