本文对应源码:https://github.com/nieandsun/NRSC-STUDY/tree/master/nrsc-unit-test-study

1 先明确一下具体问题

假设有下面这样一个技术方案:

该方案对应的伪代码如下,那该如何对下面的方法进行写单测呢?

public String transferFromAgent2Account(Object transferRequest) {

    TransactionUtil.transactional(
            () -> {
                //从代理商钱包扣款
                log.info("从代理商钱包扣款,并生成代理商维度扣款流水");
                //生成广告主充值流水
                log.info("生成广告主维度充值流水,状态为待充值");
            });

    boolean rechargeFlag = false;
    try {
        log.info("rpc -- 进行广告主钱包充值");
        accountWalletRPCService.recharge(new Object());
        //充值成功
        rechargeFlag = true;
    } catch (Exception e) {
        log.info("rpc -- 查询是否真的充值失败", e);
        boolean status = accountWalletRPCService.queryRechargeStatus(new Object());
        //充值成功
        if (status) {
            rechargeFlag = true;
        } else {
            log.info("异步重试,若仍失败,修改广告主维度充值流水状态为待重试");
        }
    }

    if (rechargeFlag) {
        log.info("修改广告主维度充值流水状态为充值成功");
        return "充值成功";
    }
    return "充值中";
}

2 答案1 -- 只看到transactional为一个静态方法

通过分析,可以看到transactional为一个静态方法,于是你可能写出的单测如下:

@RunWith(PowerMockRunner.class)
@PrepareForTest({TransactionUtil.class})
public class AgentWalletBizServiceTest {
    @Mock
    Logger log;
    @Mock
    AccountWalletRPCService accountWalletRPCService;
    @InjectMocks
    AgentWalletBizService agentWalletBizService;

    @Before
    public void setUp() throws Exception {
        //因为transactional为一个静态方法,所以你可能想着我直接将其mock掉就ok了
        PowerMockito.mockStatic(TransactionUtil.class);
        PowerMockito.doNothing().when(TransactionUtil.class, "transactional", any());
    }

    @Test
    public void testTransferFromAgent2Account_001() throws Exception {
        String result = agentWalletBizService.transferFromAgent2Account("r");
        Assert.assertEquals("充值成功", result);
    }

    @Test
    public void testTransferFromAgent2Account_002() throws Exception {
        when(accountWalletRPCService.recharge(any())).thenThrow(new RuntimeException("E"));
        when(accountWalletRPCService.queryRechargeStatus(any())).thenReturn(true);
        String result = agentWalletBizService.transferFromAgent2Account("r");
        Assert.assertEquals("充值成功", result);
    }

    @Test
    public void testTransferFromAgent2Account_003() throws Exception {
        when(accountWalletRPCService.recharge(any())).thenThrow(new RuntimeException("E"));
        when(accountWalletRPCService.queryRechargeStatus(any())).thenReturn(false);
        String result = agentWalletBizService.transferFromAgent2Account("r");
        Assert.assertEquals("充值中", result);
    }
}

跑一下单测覆盖率,你会发现,诶?为啥有两行没有覆盖到呢?😠😠😠

3 答案2 - transactional不仅是一个静态方法,而且其参数是一个Runnable

这里就要看一下PowerMockitomockStatic的另一种用法了:

//第一个参数为要mock的class
//第二个参数为给要mock的class中的某个方法(该方法并未指定)给定一个默认的答案
PowerMockito.mockStatic(TransactionUtil.class, new Answer() {
    @Override
    public Object answer(InvocationOnMock invocation) throws Throwable {
        return null;
    }
});

其中InvocationOnMock它有五个方法,分别如下:

public interface InvocationOnMock extends Serializable {

    /**
     * returns the mock object
     *
     * @return mock object
     */
    Object getMock();

    /**
     * returns the method
     *
     * @return method
     */
    Method getMethod();

    /**
     * Returns arguments passed to the method.
     *
     * Vararg are expanded in this array.
     *
     * @return arguments
     */
    Object[] getArguments();

    /**
     * Returns casted argument at the given index.
     *
     * Can lookup in expanded arguments form {@link #getArguments()}.
     *
     * @param index argument index
     * @return casted argument at the given index
     * @since 2.1.0
     */
    <T> T getArgument(int index);

    /**
     * calls real method
     * <p>
     * <b>Warning:</b> depending on the real implementation it might throw exceptions
     *
     * @return whatever the real method returns / throws
     * @throws Throwable in case real method throws
     */
    Object callRealMethod() throws Throwable;
}

可以看到,它有一个方法为Object[] getArguments(),也就是获取到方法的参数的意思,看到这里或许你就有答案了,我们只需要将<font color = blue>第2章节</font>中的 @Before对应的方法改成下面的样子就可以了:

@Before
public void setUp() throws Exception {
    //第一个参数为要mock的class
    //第二个参数为给要mock的class中的某个方法(该方法并未指定)给定一个默认的答案
    PowerMockito.mockStatic(TransactionUtil.class, new Answer() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            //将要mock的方法的参数取出,并将其转为Runnable
            Runnable runnable = (Runnable) invocation.getArguments()[0];
            //执行该Runnable对应的方法
            runnable.run();
            //这里直接返回null就OK了
            return null;
        }
    });
}

当然由于上面mockStatic的第二个参数实际上就是一个函数,所以我们也可以写成lambda表达式的样子:

@Before
public void setUp() throws Exception {
    //第一个参数为要mock的class
    //第二个参数为给要mock的class中的某个方法(该方法并未指定)给定一个默认的答案
    PowerMockito.mockStatic(TransactionUtil.class, invocation -> {
        //将要mock的方法的参数取出,并将其转为Runnable
        Runnable runnable = (Runnable) invocation.getArguments()[0];
        //执行该Runnable对应的方法
        runnable.run();
        //这里直接返回null就OK了
        return null;
    });
}

再跑一下单测,你就会很舒服的发现,我们的单测覆盖率终于为100%了。😄😄😄

4 引申 -- 参数为Callable时该怎么写单测?

这里直接上答案:

  • 假设有如下代码

    public String transferFromAgent2Account2(Object transferRequest) {
      //参数为一个Callable
      return TransactionUtil.transactional2(
              () -> {
                  //从代理商钱包扣款
                  log.info("从代理商钱包扣款,并生成代理商维度扣款流水");
                  //生成广告主充值流水
                  log.info("生成广告主维度充值流水,状态为待充值");
                  return "ok";
              });
    
    }
  • 其单测只需要这样写就OK了

    @Test
    public void testTransferFromAgent2Account_004() throws Exception {
    
      //第一个参数为要mock的class
      //第二个参数为给要mock的class中的某个方法(该方法并未指定)给定一个默认的答案
      PowerMockito.mockStatic(TransactionUtil.class, invocation -> {
          //将要mock的方法的参数取出,并将其转为Runnable
          Callable<String> callable = (Callable<String>) invocation.getArguments()[0];
    
          return callable.call();
      });
    
      String result = agentWalletBizService.transferFromAgent2Account2("r");
      Assert.assertEquals("ok", result);
    }
  • 贴一下单测覆盖率,可以看到也是100%

本文由mdnice多平台发布


nrsc
1 声望0 粉丝