背景
使用Spring Boot进行微服务或者前后端分离的相关开发设计时,通常使用接口进行对接,而此时就对项目的测试提出了相关要求,比如单元测试,继承测试,调用第三方接口测试等等,达到相应的测试覆盖率;三方接口测试需要自己进行模拟结果返回等,这时就需要Mock相应的数据返回达到测试效果。
Mock数据相关
- Mockito
- PowerMock
- TestNG
- EasyMock
- etc.
此处仅仅列举几个比较流行的,还有很多实用的Mock框架,分别有不同的特点,如PowerMock可以实现静态方法,私有方法等方法的mock;相关的mock框架大家可以自行查找,这里给出PowerMock链接(GitHub).
实践
在项目中,如果不需要对静态方法,私有方法等特殊进行验证测试,则仅仅使用Spring boot自带的Mockito即可完成相关的测试数据Mock,若需要则可以使用PowerMock,简单实用,结合Spring可以使用注解注入;
一些简单的使用这里不再赘述,可以查询相应的文章(基础文章很多),这里主要讲述的是项目中遇到的需要mock调用的第三方接口,在测试Controller层时需要跨层mock封装好的第三方接口返回数据,验证程序是否正确。(如图所示,mock数据跨层)
网上查找很多文章,大部分是重复且无用的基础使用,并无在项目中进行跨层mock数据;跨层mock因涉及到Spring的IOC容器自动注入,所以基础教程的mock不涉及跨层mock,经过实践,总结出已经在项目中使用的Mock操作;
1.使用Mockito(不涉及特殊方法Mock)
由于Mockito集成到Sring Boot的Junit测试之中,使用时不需要特别引入,直接使用即可:(代码如下)
public abstract class AbstractMockBeanTest {
/**
* BasiceServer封装了对应的三方接口服务,主要是转换接口返回结果数据
* 使用MockBean注解,自动将IOC容器中需要的对象直接替换成Mock对象,然后Mock相应数据
*/
@MockBean
protected BasicServer basicServer;
/**
* 如需 basicServer对应的方法需要返回什么类型结果,请在测试前自行调用以下对应方法
*/
protected void mockGetCarTypeSuccess() {
String msg = "{\"code\":200,\"data\":{\"businessType\":0,\"capacityType\":0,\"engineType\":0,\"id\":0," +
"\"seats\":0},\"msg\":\"success\",\"ok\":true}";
Mockito.when(basicServer.getCarType(anyLong())).thenReturn(msg);
}
protected void mockGetCarTypeSuccessNoData() {
String msg = "{\"code\":200,\"msg\":\"success\",\"ok\":true}";
Mockito.when(basicServer.getCarType(anyLong())).thenReturn(msg);
}
protected void mockGetCitySuccess() {
String msg = "{\"code\":200,\"data\":{\"businessType\":0,\"capacityType\":0,\"engineType\":0,\"id\":0," +
"\"seats\":0},\"msg\":\"success\",\"ok\":true}";
Mockito.when(basicServer.getCityByCode(anyString())).thenReturn(msg);
}
protected void MockGetCitySuccessSuccessNoData() {
String msg = "{\"code\":200,\"msg\":\"success\",\"ok\":true}";
Mockito.when(basicServer.getCityByCode(anyString())).thenReturn(msg);
}
}
2.使用PowerMock(可对特殊方法Mock)
因为跨层,当测试代码使用IOC容器自动注入相应对象时,需要手动指定将自动注入的对象替换为自己Mock的对象,才能生效;替换过程使用反射指定。(代码如下)
package cn.jasmine.capacity.apis.v1;
import cn.jasmine.capacity.application.impl.CarServiceImpl;
import cn.jasmine.capacity.third.basic.v1.BasicService;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.hamcrest.core.Is;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.modules.junit4.PowerMockRunnerDelegate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
import java.util.Optional;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.handler;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author Jasmine
* @date 19/06/19
*/
@ActiveProfiles("test")
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringRunner.class)
@PrepareForTest({BasicService.class})
@PowerMockIgnore({"javax.management.*", "javax.net.ssl.*"})
@SpringBootTest
public class TestController {
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@Mock
private BasicService basicService;
@Autowired
private CarServiceImpl carService;
@Before
public void setUp() throws Exception {
mvc = MockMvcBuilders.webAppContextSetup(context).build();
// 使用反射将IOC容器注入的service的属性替换指定为mock的对象
ReflectionTestUtils.setField(carService, "basicService", basicService);
PowerMockito.when(this.basicService, "getCarType", anyLong()).thenReturn(Optional.empty());
}
@Test
@Transactional
public void testA() throws Exception {
MvcResult result = mvc.perform(MockMvcRequestBuilders.get("/v1/cars")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON)
).andExpect(status().isOk())
.andExpect(handler().handlerType(CarController.class))
.andExpect(handler().methodName("search"))
.andDo(MockMvcResultHandlers.print())
.andReturn();
JSONObject object = JSON.parseObject(result.getResponse().getContentAsString(), JSONObject.class);
Assert.assertThat("数据量不是2", object.getJSONObject("data").getJSONArray("list").size(), Is.is(2));
}
}
3. 实际应用
场景:
- SpringBoot 微服务项目
- 业务层service与dao层测试(含db【mysql】) 实例:
- mysql单测时使用内存数据库H2代替
- 配置build.gradle文件(maven按照指定格式添加)
/**
* 测试使用
*/
testRuntimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testCompile group: 'org.powermock', name: 'powermock-module-junit4', version: '2.0.0'
testCompile group: 'org.powermock', name: 'powermock-api-mockito2', version: '2.0.0'
- 配置application-test.yml文件
# 使用H2代替MySQL
spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=MySQL
schema: classpath:init_table.sql
data: classpath:init_data.sql
- service因为含有dao层操作,为了实现dao层测试覆盖,不能草草mock完事,否则无法准确验证;单测不mock dao层操作,使用spring启动
// ……
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.modules.junit4.PowerMockRunnerDelegate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.List;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
/**
* 上报信息保存
*
* @author Jasmine
* @date 19/06/18
*/
@ActiveProfiles("test")
@RunWith(PowerMockRunner.class)
// 单元测试需启动spring
@PowerMockRunnerDelegate(SpringRunner.class)
@PowerMockIgnore("javax.management.*")
@PrepareForTest(CarReportInfoService.class)
@SpringBootTest
@Transactional
public class CarReportInfoServiceImplTest extends AbstractServiceTest {
@Autowired
private CarReportInfoService reportInfoService;
@Autowired
private CarReportInfoMapper reportInfoMapper;
@Test
public void save() {
// 模拟请求参数
// 调用mapper数据
// 返回结果
String driverNo = "10000";
double lon = 120.234453;
CartReportInfoCreate reportInfoRequest = new CartReportInfoCreate();
reportInfoRequest.setDriverNo(driverNo);
reportInfoRequest.setLon(lon);
reportInfoRequest.setLat(120.234643);
reportInfoRequest.setReportTime(new Date());
reportInfoRequest.setPhoneVersion("IOS");
reportInfoRequest.setAppVersion("12.2");
reportInfoRequest.setReportType(0);
reportInfoRequest.setReportAddress("胜利大街");
reportInfoService.create(reportInfoRequest);
List<CarReportInfoEntity> carReportInfos = reportInfoMapper.selectAll();
int sizeCount = carReportInfos.size();
assertThat("司机编号错误", carReportInfos.get(sizeCount - 1).getDriverNo(), equalTo(driverNo));
// 上报重复
try {
reportInfoService.create(reportInfoRequest);
} catch (CapacityRuntimeException e) {
assertEquals(e.getCode(), StatusCode.ALREADY_REPORTED.getCode());
}
}
/**
* 少传递参数抛出异常
*/
@Test(expected = DataIntegrityViolationException.class)
public void saveException() {
String driverNo = "456789876768684342";
double lon = 120.234453;
CartReportInfoCreate reportInfoRequest = new CartReportInfoCreate();
reportInfoRequest.setDriverNo(driverNo);
reportInfoRequest.setLon(lon);
reportInfoRequest.setLat(120.234643);
reportInfoRequest.setReportTime(new Date());
// reportInfoRequest.setAppType("IOS");
reportInfoRequest.setAppVersion("12.2");
reportInfoService.create(reportInfoRequest);
}
/**
* 测试获取时间段内听单时长
*/
@Test
public void getListenDuration() throws Exception {
String driverNo = "345678976543";
// 时间段没有上报记录,之前有出车记录()
// Instant instantSpec = LocalDateTime.of(2019, 6, 25, 19, 00, 9).atZone().toInstant();
Instant instantSpec = Instant.parse("2019-06-25T19:00:09.00Z");
PowerMockito.mockStatic(Instant.class);
PowerMockito.when(Instant.now()).thenReturn(instantSpec);
// 时间段在当前时间之后,返回0
String startString = "2019-06-25T10:00:09.00Z";
String endString = "2019-06-25T10:01:09.00Z";
PowerMockito.when(Instant.parse(startString))
.thenReturn(DateTimeFormatter.ISO_INSTANT.parse(startString, Instant::from));
PowerMockito.when(Instant.parse(endString))
.thenReturn(DateTimeFormatter.ISO_INSTANT.parse(endString, Instant::from));
CarReportPageSearch request = new CarReportPageSearch();
request.setStartDate(Instant.parse(startString));
request.setEndDate(Instant.parse(endString));
Long listenDuration = reportInfoService.getListenDuration(driverNo, request);
assertThat("时间错误", listenDuration, is(32400L));
// 时间段没有上报记录,之前有出车记录()
request = new CarReportPageSearch();
startString = "2019-06-25T18:50:09.00Z";
endString = "2019-06-25T19:00:09.00Z";
PowerMockito.when(Instant.parse(startString))
.thenReturn(DateTimeFormatter.ISO_INSTANT.parse(startString, Instant::from));
PowerMockito.when(Instant.parse(endString))
.thenReturn(DateTimeFormatter.ISO_INSTANT.parse(endString, Instant::from));
request.setStartDate(Instant.parse(startString));
request.setEndDate(Instant.parse(endString));
listenDuration = reportInfoService.getListenDuration(driverNo, request);
assertThat("时间错误", listenDuration, equalTo(600L));
// 时间段没有上报记录,之前有收车记录
request = new CarReportPageSearch();
startString = "2019-06-25T18:55:10.00Z";
endString = "2019-06-25T19:00:09.00Z";
PowerMockito.when(Instant.parse(startString))
.thenReturn(DateTimeFormatter.ISO_INSTANT.parse(startString, Instant::from));
PowerMockito.when(Instant.parse(endString))
.thenReturn(DateTimeFormatter.ISO_INSTANT.parse(endString, Instant::from));
request.setStartDate(Instant.parse(startString));
request.setEndDate(Instant.parse(endString));
listenDuration = reportInfoService.getListenDuration(driverNo, request);
assertThat("时间错误", listenDuration, is(299L));
// 非正常情况(收车-出车)
request = new CarReportPageSearch();
Instant start21 = LocalDateTime.of(2019, 6, 25, 17, 54, 10).atZone(ZoneId.of("UTC")).toInstant();
Instant end22 = LocalDateTime.of(2019, 6, 25, 18, 55, 10).atZone(ZoneId.of("UTC")).toInstant();
Instant instant21 = LocalDateTime.of(2019, 6, 25, 17, 55, 9).atZone(ZoneId.of("UTC")).toInstant();
Instant instant22 = LocalDateTime.of(2019, 6, 25, 18, 55, 9).atZone(ZoneId.of("UTC")).toInstant();
startString = "2019-06-25T17:54:10.00Z";
endString = "2019-06-25T18:55:10.00Z";
PowerMockito.when(Instant.parse(startString))
.thenReturn(DateTimeFormatter.ISO_INSTANT.parse(startString, Instant::from));
PowerMockito.when(Instant.parse(endString))
.thenReturn(DateTimeFormatter.ISO_INSTANT.parse(endString, Instant::from));
request.setStartDate(Instant.parse(startString));
request.setEndDate(Instant.parse(endString));
listenDuration = reportInfoService.getListenDuration(driverNo, request);
long allTime = ChronoUnit.SECONDS.between(start21, instant21) + ChronoUnit.SECONDS.between(instant22, end22);
assertThat("时间错误", listenDuration, equalTo(3959L));
}
}
总结
PowerMock相比Mockito提供更多的Mock方法,使用相对灵活,推荐使用。
使用PowerMock现在对应的gradle依赖为,使用powermock-api-mockito2,不要使用powermock-api-mockito:
testCompile group: 'org.powermock', name: 'powermock-module-junit4', version: '2.0.2'
testCompile group: 'org.powermock', name: 'powermock-api-mockito2', version: '2.0.2'
testCompile group: 'org.mockito', name: 'mockito-core', version: '2.28.2'
注:使用powermock高版本会存在一些bug
详细的可以参考以下链接: