背景
作为开发人员,在代码交付QA前,为了保证交付质量和代码正确性,一般对代码进行单元测试。单测一般由Mock和断言两部分组成,大部分情况下,我们会针对要测试类的成员对象方法调用的返回值进行Mock,然后通过断言去判断方法的逻辑是否符合预期。但是一些情况下,我们会发现一些代码的返回值是Void这样的话我们便无法根据返回值进行断言操作,此外还有一些方法可能含有中途返回的Case即在某些情况下直接返回了,不执行接下来的逻辑,这样的也无法直接通过断言工具去判断方法逻辑的准确性。这时候,我们就需要用到Mock框架的一些功能来进行校验,本文以Mockito为例,来展示如何对这些场景进行单元测试。
原理
一个方法有三个组成部分,入参、逻辑以及返回值,单测便可由这三个部分入手。而入参是决定执行逻辑的,所以一般情况下我们可以针对逻辑和单测进行单元测试。大部分情况下,逻辑由Mock工具掌管,而返回值则依靠断言工具管理。在没有返回值的情况下,通过断言验证的方法走不通,那么就可以从逻辑的角度入手通过Mock工具来验证逻辑是否执行正确。由于在进行单元测试的情况下,我们一般会对底层调用用Mock对象屏蔽,而通过Mock框架比如Mockito进行Mock时,在方法运行后,Mock对象的交互情况是有记录的,所以我们可以通过这些Mock对象的调用信息来判断代码逻辑的正确性。
对于Mockito我们可以从Verify的底层实现方法org.mockito.internal.MockitoCore#verify入手,Mockito提供的verifyNoInteractions等方法的基础实现皆是该方法。具体代码如下:
public <T> T verify(T mock, VerificationMode mode) {
if (mock == null) {
throw nullPassedToVerify();
}
MockingDetails mockingDetails = mockingDetails(mock);
if (!mockingDetails.isMock()) {
throw notAMockPassedToVerify(mock.getClass());
}
assertNotStubOnlyMock(mock);
MockHandler handler = mockingDetails.getMockHandler();
mock = (T) VerificationStartedNotifier.notifyVerificationStarted(
handler.getMockSettings().getVerificationStartedListeners(), mockingDetails);
MockingProgress mockingProgress = mockingProgress();
VerificationMode actualMode = mockingProgress.maybeVerifyLazily(mode);
mockingProgress.verificationStarted(new MockAwareVerificationMode(mock, actualMode, mockingProgress.verificationListeners()));
return mock;
}
从以上定义我们可以看出verify接口是对Mock对象的VerificationMode校验模式进行校验。而VerificationMode是一个接口其方法如下:
public interface VerificationMode {
/**
* 这个是主要实现方法,verifycationData包含了Mock对象的调用信息,可根据调用信息来实现自己的校验方法
*/
void verify(VerificationData data);
VerificationMode description(String description);
}
Mockito自带了一些该接口的实现,我们可以通过VerificationModeFactory这个类找到他们,大部分是关于调用信息的,如调用次数等。参考这些接口的实现,自己也能实现一些校验模式。
实践
比如针对如下这段代码一个常见的幂等处理方法,业务背景不仔细介绍了,大概流程是对于数据的uuid已经消费过的的情况跳过不执行逻辑,没有消费过的则要继续执行保存逻辑。这段方法有两个显著特点,一是返回值为void,二是存在中途跳出逻辑的情况,这种情况下,针对这段代码,我们需要写两个单测case来确保逻辑是正确的。即
uuid不存在,需要确保对数据进行保存操作,且保存的值符合预期。
uuid已经存,接口幂等不做保存处理,仅打印日志。
@Override
@Transactional(rollbackFor = Throwable.class)
public void saveOrder(List orders) {
Map<String, List> orderMap = orders.stream().collect(Collectors.groupingBy(Order::getUuid));
for (String uuid : orderMap.keySet()) {
if (exists(uuid, orderMap.get(uuid))) {
log.error(“接收单据uuid重复,{}”, uuid);
// 重复跳过,不抛异常
continue;
}
orderDao.insertList(convert(orderMap.get(uuid)));
List orderDetails = orderMap.get(uuid)
.stream()
.map(OrderDetail::getOrderDetails)
.flatMap(Collection::stream)
.collect(Collectors.toList());
orderDetailDao.insertList(convertDetails(orderDetails));
}
}
对于这种void的返回值,并且也没有抛异常的出现,我们无法对返回值进行断言。而且关键是由于流程有跳过的可能,使用断言框架是无法验证这种流程的。但由于我们这个逻辑中的对象是有Mock对象的即OrderDao和OrderDetailDao,所以我们可以利用Mockito的verify校验功能对单测的Mock对象的交互情况做一个断言处理,而这个就依赖于Mockito的verify功能。
下面代码表示是针对case1即不存在原uuid,这样我们需要确保有交互并且交互数据和预期一致,这里使用verify+ArgumentCaptors的对Mock对象的入参进行抓取,然后使用再使用断言工具判断入参是否符合预期。其实个人认为用verify+ArgumentMathers的方法更正确,因为这里是对逻辑校验单纯使用Mock框架将更明显验证这一点,但为了更好看还是使用了Mock+断言的方式验证方法。
@Test
@DisplayName("保存数据不存在原uuid")
void testSaveOrderNotExist() {
Order order = new Order();
order.setOrderNo("son1");
order.setUuid("son1");
order.setOrderDetails(Collections.singletonList(new OrderDetail()));
OrderPo orderPo = new OrderPo();
orderPo.setOrderNo("son1");
orderPo.setUuid("son1");
when(orderDao.insertList(Collections.singletonList(orderPo))).thenReturn(1);
when(orderDetailDao.insertList(anyList())).thenReturn(1);
Uuid bizUuid = new Uuid();
bizUuid.setBusinessNo("son1");
bizUuid.setUuid("son1");
bizUuid.setOperateType(TaskAssignConstant.INIT_ORDER_OPERATE_TYPE);
// 这里的mock返回值影响exist方法的返回值1代表未存在
when(taskAssignUuidDao.insertIgnore(bizUuid)).thenReturn(1);
orderRepositoryImpl.saveOrder(Collections.singletonList(order));
/*
* 这里使用Mockito的verify方法通过ArgumentCaptor对mock对象orderDao的入参进行抓取,
* 然后通过断言判断该Mock对象的交互参数是否符合预期,使用ArgumentCaptor可以抓取参数通过断言判断。
* 也可直接对入参进行构造,将使用对象的equals方法进行判断,也可使用ArgumentMathers构造一个匹配参数方法验证。
*/
ArgumentCaptor<List<OrderPo>> argumentCaptor = ArgumentCaptor.forClass(List.class);
verify(orderDao).insertList(argumentCaptor.capture());
OrderPo orderPo1 = argumentCaptor.getValue().get(0);
Assertions.assertEquals("son1", orderPo1.getOrderNo());
Assertions.assertEquals("son1", orderPo1.getUuid());
}
下图针对case2,即存在原uuid,由于原代码存在uuid直接continue相当于跳过了下面的流程,所以需要使用verfiy校验mock的对象在这个case执行时没有交互。
@Test
@DisplayName("保存数据存在原uuid")
void testSaveorderExist() {
order order = new order();
order.setorderNo("son1");
order.setUuid("son1");
order.setWarehouseNo("6_6_618");
orderPo orderPo = new orderPo();
orderPo.setorderNo("son1");
orderPo.setUuid("son1");
orderPo.setWarehouseNo("6_6_618");
when(orderDao.insertList(Collections.singletonList(orderPo))).thenReturn(1);
when(orderDetailDao.insertList(any())).thenReturn(0);
Uuid bizUuid = new Uuid();
bizUuid.setWarehouseNo("6_6_618");
bizUuid.setBusinessNo("son1");
bizUuid.setUuid("son1");
bizUuid.setOperateType(TaskAssignConstant.INIT_ORDER_OPERATE_TYPE);
when(taskAssignUuidDao.insertIgnore(bizUuid)).thenReturn(0);
orderRepositoryImpl.saveorder(Collections.singletonList(order));
// 使用verifyNoInteractions 校验mock对象在uuid已存在的情况下应该没有交互
verifyNoInteractions(orderDao);
verifyNoInteractions(orderDetailDao);
}
推荐阅读:
https://haokan.baidu.com/v?vid=13129570371347966003
https://haokan.baidu.com/v?vid=8996454114194279875
http://www.iqiyi.com/v_1qz7s2wlmiw.html
http://www.iqiyi.com/v_dw4wejp4i8.html
https://v.qq.com/x/page/u3305v926vr.html
https://v.qq.com/x/page/b3305wnb4hu.html
http://my.tv.sohu.com/us/368555017/299042955.shtml
http://baijiahao.baidu.com/builder/preview/s?id=1714923737881011833
http://baijiahao.baidu.com/builder/preview/s?id=1714921630416904310
http://baijiahao.baidu.com/builder/preview/s?id=1714919012468000006