- 背景
我要测试某个方法,但是这个方法有很多的依赖,如HSF服务依赖、数据库依赖等,本来写好的时候测试用例跑的好好的,结果某一天就呵呵了,要么是数据库中的测试数据被改了,要么是依赖的HSF服务关闭了(大哥,我在部署我的应用呢,对不住了),当然,有的时候心血来潮,要体验下测试驱动开发,哥们你作为一只老猿/媛,piapiapia,两下子搞完了,你的小伙伴只能眼睁睁的看着完全跟不上你的节奏啊,他提供不了你需要的数据,咋办?为了解决这些痛点,我们需要对这些外部调用都进行mock,好了,下面扯扯我的mock选择之路吧。
- Mockito篇
很不开心的表示以前写的测试用例应该都属于集成测试,初次接触mock框架,两眼一抹黑,看到小伙伴在用着mockito,依葫芦画瓢,kaka写上几个再说。
被测试方法:
public ExtTicketInfoModel buildExtTicketInfoModel(QueryTicketInfoResponse response,
TicketInfoRequest ticketInfoRequest,
TerminalInfo terminalInfo) {
******
List<VoucherEntity> voucherEntities = voucherRepository.queryVoucherByCodesAndCinema(null,
fetchCode, cinemaId, null, partnerCode);
******
}
测试用例:
//指定mock执行框架
@RunWith(MockitoJUnitRunner.class)
public class BuildExtTicketInfoModelTest extends AbstractUnitBase {
// 被测试的类
@InjectMocks
private ChenxingServiceHelper chenxingServiceHelper = new ChenxingServiceHelper();
// 需要mock的外部依赖
@Mock
private VoucherRepository voucherRepository;
@Test
public void testBuildExtTicketInfoModel() throws Exception {
// build request
String responseXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><QueryTicketInfoResult><ResultCode>0</ResultCode><Message>成功</Message>******</QueryTicketInfoResult>";
Serializer serializer = new Persister();
QueryTicketInfoResponse response = serializer.read(QueryTicketInfoResponse.class, responseXml, false);
TicketInfoRequest ticketInfoRequest = new TicketInfoRequest();
ticketInfoRequest.setExtCinemaNo("12345678");
ticketInfoRequest.setFetchCode("12345678");
TerminalInfo terminalInfo = new TerminalInfo();
terminalInfo.setRouteCode("chenxing");
// mock response object
List<VoucherEntity> vouchers = new ArrayList<VoucherEntity>();
VoucherEntity voucher = new VoucherEntity();
voucher.setId(1L);
voucher.setTbOrderId(111L);
voucher.setGmtCreate(new Date());
vouchers.add(voucher);
// stub,俗称打桩,指的是当被测试的方法中调用了该外部依赖时,该外部依赖将被mock
Mockito.when(
voucherRepository.queryVoucherByCodesAndCinema(anyString(), anyString(), anyLong(), anyString(),
anyString())).thenReturn(vouchers);
ExtTicketInfoModel result = chenxingServiceHelper.buildExtTicketInfoModel(response,
ticketInfoRequest, terminalInfo);
println(result);
Assert.assertNotNull(result);
Assert.assertTrue(result.getExtTicketModels().size() > 0);
Assert.assertEquals(TerminalConstants.TAOBAO_SELLER_NAME, result.getExtVoucherModel().getSellerName());
}
}
针对通常的依赖于接口的调用mockito用着很顺利,然而,当我写着写着,发现我有个待测试方法居然调用了个外部静态方法,找来找去也没发现mockito有相关的mock能力,继续挖挖挖,矮油,powermock这货不错,居然可以mock静态方法,私有方法好像很强大的样子嘛。
- powermock篇
这里引用一篇文章,学习时所参考的,很不错,写的很详细,我就不再缀诉了
(这里没贴地址,内网地址)
然而当我写完之后发现,我又有了个新的需求,我想要知道我的代码覆盖率如何了,总不能每写几个,就上amon看看我的代码覆盖率吧,这样效率真心有点low,继续挖,铛铛铛,还真有个niubility的东西,jmockit,不但拥有powermock的所有功能,而且还能统计代码的覆盖率。
- jmockit篇
maven依赖:
<!-- jmockit -->
<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.23</version>
<scope>test</scope>
</dependency>
<!-- 如果需要统计覆盖率,则需要加上如下包 -->
<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit-coverage</artifactId>
<version>1.23</version>
<scope>test</scope>
</dependency>
上码:
被测试方法:
public ResultGeneralModel<?> queryVoucherInfo(TerminalInfo terminalInfo,
TicketInfoRequest ticketInfoRequest) {
******
try {
// 查询票据接口
String responseXml = ClientUtil.queryTicketInfo(requestXML, chenxingPartnerDetail.getUrl());
if (StringUtil.isBlank(responseXml)) {
return null;
}
Serializer serializer = new Persister();
response = serializer.read(QueryTicketInfoResponse.class, responseXml, false);
} catch (Exception e) {
throw new TFTerminalRuntimeException(CommonErrorCodeEnum.SYSTEM_ERROR,
"Fetch chenxing VoucherInfo,ticketInfoRequest:" + ticketInfoRequest, e);
}
******
ResultGeneralModel<ExtTicketInfoModel> queryResult = chenxingServiceHelper
.convertQueryResult(response, extTicketInfoModel);
return queryResult;
}
测试用例:
// 指定Jmockit,这样在启动的时候会自动进行初始化的操作
@RunWith(JMockit.class)
public class QueryVoucherInfoTest extends AbstractUnitBase {
// 被测试的类
@Tested
private ChenxingPartnerServiceImpl service;
@Injectable
private PartnerComponent partnerComponent;
// 外部接口依赖
@Injectable
private ChenxingServiceHelper chenxingServiceHelper;
// 依赖的静态方法
@Mocked
private ClientUtil clientUtil;
private static final String partnerCode = "chenxing";
@SuppressWarnings("static-access")
@Test
public void testQueryVoucherInfo() throws Exception {
TicketInfoRequest ticketInfoRequest = new TicketInfoRequest();
ticketInfoRequest.setExtCinemaNo("12345678");
ticketInfoRequest.setFetchCode("12345678");
TerminalInfo terminalInfo = new TerminalInfo();
terminalInfo.setRouteCode(partnerCode);
// 录制预期值
new Expectations() {
{
// mock 静态方法调用
clientUtil.queryTicketInfo(anyString, anyString);
result = mockQueryTicketInfoResponseXml();
// mock普通接口依赖
chenxingServiceHelper.convertQueryResult((QueryTicketInfoResponse)any, (ExtTicketInfoModel)any);
result = mockConvertQueryResult();
}
};
ResultGeneralModel<?> result = service.queryVoucherInfo(terminalInfo, ticketInfoRequest);
println("Chenxing QueryVoucherInfo-正常", result);
Assert.assertNotNull(result);
Assert.assertEquals(result.getReturnCode(), "1");
}
}
// 很多时候我们需要了解异常情况下程序执行会如何,而异常情况又很难得到,这时候也可以使用mock,录制片段如下:
clientUtil.queryTicketInfo(anyString, anyString);
result = new SocketTimeoutException("调用远程服务超时");
直接new出来即可,是不是很简洁。
2016-10-08补充:
发现jmockit mock之后,被测试的类打了断点,但是却进入不了断点的,前两天实在纠结,就特意查了一下,有了如下解决方案:先在test方法中打好断点,启动测试用例,再到被测试方法中打断点,这样被测试类的断点就会起效了。
原理:因为jmockit的mock是通过修改字节码来达到目的的,所以在启动前打的断点的类并不是实际应用执行的类,只有待启动后,类加载器加载后去打断点才能真正打在被修改字节码后的类上(有点绕),参考:http://stackoverflow.com/questions/28594487/debugging-java-code-tested-with-spock-and-jmockit
覆盖率统计:mvn test或直接在IDE中执行完test项目之后,会自动生成覆盖率统计文件,目录下有index.html,直接查看就可。
JMockit: Coverage report written to D:\work\workSpace\projectName\test\target\coverage-report
至此在初步完了3个mock框架之后,最终以选择jmockit而告终,只因其强大和简洁。
- 留在最后的话
在使用过程中碰上一种场景,不知道这样的代码如何进行测试覆盖:
public class AService {
private BService bService;
public void getUserProp() {
Model model = new Model();
bService.changeModel(model);
if (model.getUserId() != null && model.getUserId().longValue() == 888888) {
System.out.println("我是888888");
}
}
}
public class BService {
public void changeModel(Model model) {
Random random = new Random();
Long userId = random.nextLong();
model.setUserId(userId);
}
}
以上这种传递对象,对象被更改,然后再之后又会使用对象中的某些值来实现某些逻辑的又如何mock呢,求了解的同学支高招。