一、Mock的含义
Mock在英语的释义中,动词是嘲笑、嘲弄、模仿的意思,而形容词/名词的释义更贴合程序语言设计中Mock的含义,not real but appearing or pretending to be exactly like something,即不是真的但是假装它和某物很相似。
为什么要Mock呢,去假装模拟一个东西,但是又不是真的?这是因为很多时候,真实的东西难以获得,考虑到成本、效率的问题,我们常常希望在测试的较早阶段(比如开发者测试),能够低成本的模拟场景即可,而不必真正在真实场景中进行测试。
在程序开发中,Mock技术一般两种场景用的比较多,一种是单元测试的时候,另一种是接口测试的时候。接口测试,比如说后端开发中,常常用的Postman,可以理解成一种接口的Mock工具,我们可以人为的构造请求,模拟真实的请求,来进行接口测试。这里我们着重讲单元测试的Mock。
Mock分类
Mock可以分为两大类,一种是Mock静态方法,另一种是Mock实例方法。Mock实例方法通过使用@Mock注解类,而Mock静态方法,则需要使用mockStatic()方法,后面会结合代码实例讲解这两类Mock如何使用。常用的Java Mock框架Mockito在3.4.0之前的版本不支持mock静态方法,更高版本则提供了该特性
Mock的框架
Mock常用的框架比较多的是Mockito和PowerMock,由于SpringBoot的默认Mock框架就是Mockito,无需引入额外的三方件,根据最小依赖原理(奥卡姆剃刀),我们这里主要探讨Mockito框架。
Mockito 是一种 Java Mock 框架,主要是用来做 Mock 测试,它可以模拟任何 Spring 管理的 Bean、模拟方法的返回值、模拟抛出异常等等,同时也会记录调用这些模拟方法的参数、调用顺序,从而可以校验出这个 Mock 对象是否有被正确的顺序调用,以及按照期望的参数被调用。
像是 Mockito 可以在单元测试中模拟一个 Service 返回的数据,而不会真正去调用该 Service,这就是上面提到的 Mock 测试,也就是通过模拟一个假的 Service 对象,来快速的测试当前我想要测试的类。
目前在 Java 中主流的 Mock 测试工具有 Mockito、JMock、EasyMock等等,而 SpringBoot 目前内建的是 Mockito 框架。
Java的单元测试中,我们可以引入mockito框架来帮助我们实现mock,需要的pom依赖如下:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
@Mock和@InjectMocks的区别
https://stackoverflow.com/questions/16467685/difference-between-mock-and-injectmocks/46727655
@Mock创建一个mock,@InjectMocks创建一个实例并且将注入由@Mock或者@Spy注解生成的mock。
注意,需要通过@Runwith(MockitoJUnitRunner.class)或者@Mockito.initMocks(this)来初始化这些mocks并注入。
如下所示,一般而言,是将需要测试的类,注解为@InjectMocks
,将其依赖注解为@Mock
@RunWith(MockitoJUnitRunner.class)
public class SomeManagerTest {
@InjectMocks
private PlayGameService playGameService;
@Mock
private IUserDatabaseDao iUserDatabaseDao; // playGameService有调用数据库查询的DAO,将数据库查询的DAO注入到playGameService中
//tests...
}
二、Mockito测试静态方法
在类HecWafUtill.java中有一个静态方法blockHecWaf,我们希望对该方法进行单元测试,即测试方法HecWafUtil.blockHecWaf()
这个Util类中实现了一个对一些恶意IP进行封堵的操作,但是需要调用第三方的WAF的接口,我们在单元测试中,不希望有连接三方系统的操作,因为环境无法保证网络是通的,其次我们也不希望单元测试对实际业务造成影响。所以这里我们可以对HttpRequest的post方法进行Mock
public class TestUtil {
public static HttpResponse<String> blockHecWaf(String ip) {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("ip", ip);
HttpRequest request = HttpRequest.post("xxx/api/waf", requestBody);
HttpResponse<String> res = request.execute("<service_id>");
return res;
}
}
UT代码示例
@RunWith(MockitoJUnitRunner.class)
public class MicroServiceUtilTest {
@Mock
private HttpRequest request;
@Test
public void test() {
// 模拟静态HttpRequest
MockedStatic<HttpRequest> httpRequestMockedStatic = mockStatic(HttpRequest.class);
// 模拟HttpRequest的静态方法HttpRequest.post()
httpRequestMockedStatic.when(() -> HttpRequest.post(anyString(), anyMap())).thenReturn(request);
// 构造测试对象
HttpResponse<String> httpResponse = new HttpResponse<>();
httpResponse.setResult("test123");
when(request.execute(anyString())).thenReturn(httpResponse);
// 调用静态方法
HttpResponse<String> response = MicroServiceUtil.blockHecWaf("192.168.5.5");
Assert.assertEquals("test123", response.getResult());
httpRequestMockedStatic.close();
}
}
除了使用 httpRequestMockedStatic.close()
来手动关闭资源,我们更多的是使用 try-with-resource
语法来实现资源的自动关闭,这种写法更加简洁安全。
public void test() {
try(MockedStatic<HttpRequest> httpRequestMockedStatic = mockStatic(HttpRequest.class)) {
...
}
}
三、Mockito测试实例方法
待测试的类TestService
@Component
public class TestService extends BaseHttpService<TestRequest> {
@Autowired
private TenantUtil tenantUtil;
@Override
public CommonResponse<String> execute(TestRequest TestRequest) throws HttpException {
String tenantId = tenantUtil.getCurrentTenantId();
CommonResponse<String> commonResponse = new CommonResponse<>().setResult(tenantId);
return commonResponse;
}
}
我们这里Mock了一下,当运行到tenantUtil.getCurrentTenantId()
时直接返回租户的ID为2000,所以最终返回的commmonResponse的result的值就是2000
@RunWith(MockitoJUnitRunner.class)
public class TestServiceTest {
@InjectMocks
private TestService service;
@Mock
private TenantUtil tenantUtil;
@Mock
private TestRequest testRequest;
@Test
public void test() throws Exception {
when(tenantUtil.getCurrentTenantId()).thenReturn("2000");
CommonResponse<String> response = service.execute(testRequest);
Assert.assertEquals("2000", response.getResult());
}
}
四、测试void方法
有一个方法,他的返回值是void,也就是说,我们无法对方法的返回值进行断言。那么我们要如何测试void方法呢?参考这篇博客里的说法,我觉得说的挺好的
https://blog.csdn.net/dongjian764/article/details/17303279
首先站在测试角度来分析void方法, 每一个方法的设计都有特定功能的,在下面列一些void方法可能作的事:在方法体内对私有的对象实例进行属性设置、作了日志输出、调用另外实例方法、初始化某个类型变量(可能是私有的或者全局的)、调用了其他微服务的接口、进行了数据库的增删改查的操作、对一些异常情景抛出了异常。
虽然我们获取不到void方法最终的返回值,但是我们可以对void方法内部的行为的影响(也叫做副作用)去做测试分析。具体来说,我们可以使用如下几个方法来对void方法进行单元测试:
- 获取实例对象,验证其变化是否符合预期
- 使用verify()方法,验证void函数内部某个方法执行的次数。
- 使用doAnswer()方法,验证void函数内部调用某方法时的入参
- 使用AgumentCaptor捕获调用某些方法时的入参,验证入参在是否符合预期
- 使用assertThrows,验证void方法中的异常分支
现在举一些代码实例来进行说明:
获取实例对象,验证其变化是否符合预期
最经典的就是对Java bean的set()方法的单元测试(虽然我个人觉得对bean的单元测试没有什么意义,但是行覆盖率要统计呀),这里我们想覆盖测试到这个bean的setAgentId方法,它虽然没有返回值,但是它设置了AssetHost对象里面的agentId的值,所以我们只需要验证它set的值是否符合预期就行,这个是不是很好理解检验“副作用”的这个意思。
待测试的bean
public class AssetHost {
private String agentId;
public String getAgentId() {
return agentId;
}
public void setAgentId(String agentId) {
this.agentId = agentId;
}
}
UT代码
@Test
public void testAssetHost() {
AssetHost assetHost = new AssetHost();
assetHost.setAgentId("da9e46c7-e275-4286-9ce9-8ecaa3864530");
String hostId = assetHost.getAgentId();
assertEquals("da9e46c7-e275-4286-9ce9-8ecaa3864530", hostId);
}
使用verify()方法,验证void函数内部某个方法执行的次数
就比如这个测试方法对iScoreDao这个数据库DAO,执行数据插入更新次数进行校验
iScoreDao.batchInsertOrUpdateScore()
会执行3次
@RunWith(MockitoJUnitRunner.class)
public class SynJobTest {
@InjectMocks
private SynJob synJob;
@Test
public void testSynScore() throws JobExecutionException {
try (MockedStatic<MicroServiceUtil> microServiceUtilMockedStatic = mockStatic(MicroServiceUtil.class);
.......
synJob.execute(jobExecutionContext);
verify(iScoreDao, times(3)).batchInsertOrUpdateScore(anyList());
}
}
}
使用doAnswer()方法,验证void函数内部调用某方法时的入参
待测试的类
public class Sample {
public void upsertDetail(String summary) {
List<Detail> details = buildDetail(summary);
iDetailDao.batchUpsert(details);
}
}
UT代码,使用doAnswer可以拿到方法执行到iDetailDao.batchUpsert的入参details,进而内部的元素进行断言,进而校验是否符合预期值
@RunWith(MockitoJUnitRunner.class)
public class SampleTest {
@InjectMocks
private Sample sample;
@Test
public void test() {
doAnswer(invocationOnMock -> {
List<Detail> details = invocationOnMock.getArgument(0); // 获取入参,跟数组的一样,起始计数为0,即获取最开始的那个入参
Detail detail = details.get(0); // 取数组首项
assertEquals("Success", detail.getResult()); // 预期 result = Success
return null;
}).when(iDetailDao).batchUpsert(any());
sample.upsertDetail("summary string");
}
}
使用AgumentCaptor捕获调用某些方法时的入参,验证入参在是否符合预期
参考ArgumentCaptor使用方法:
https://blog.csdn.net/hotdust/article/details/51417163
待测试的类
public class Sample {
public void upsertDetail(String summary) {
List<Detail> details = buildDetail(summary);
iDetailDao.batchUpsert(details);
}
}
使用参数捕获器验证入参,ArgumentCaptor要结合verify()使用,和doAnswer()...when()方法不同,需要将verify放在sample.upsertDetail
后面执行。
argumentCaptor.getValue()
方法可以捕获到到最后一次调用方法的入参,从而进行断言,验证参数是否符合预期。若有多次方法的调用,可以使用getAllValues()
方法获取参数列表。具体ArgumentCaptor可参阅网上的资料。
@RunWith(MockitoJUnitRunner.class)
public class SampleTest {
@InjectMocks
private Sample sample;
@Test
public void test() {
sample.upsertDetail("Summary string");
ArgumentCaptor<List<Detail>> argumentCaptor = ArgumentCaptor.forClass(List.class);
verify(iDetailDao, times(1)).batchUpsert(argumentCaptor.capture());
List<Detail> details = argumentCaptor.getValue();
assertEquals("192.168.X.X", details.get(0).getIp());
}
}
使用assertThrows,验证void方法中的异常分支
public class Sample {
public void upsertDetail(String summary) throws HttpException {
List<Detail> details = buildDetail(summary);
iDetailDao.batchUpsert(details);
}
}
假设传入的某些字符串会导致upsertDetail
方法会抛出HttpException异常,我们使用assertThrows
断言可以检查特定情况下的的方法是否有抛出特定异常,从而达到验证void方法异常分支的目的。
@RunWith(MockitoJUnitRunner.class)
public class SampleTest {
@InjectMocks
private Sample sample;
@Test
public void test() throws HttpException {
HttpException httpException = assertThrows(HttpException.class,
() -> sample.upsertDetail("<some string cause exception>"));
assertEquals("message", httpException.getMessage());
assertEquals(404, httpException.getHttpStatus());
}
}
本博客文章除特别声明外,均可自由转载与引用,转载请标注原文出处:http://www.yelbee.top/index.php/archives/202/