单测书写返回值为空应该怎么处理

背景
作为开发人员,在代码交付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

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值