本文对应源码: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
这里就要看一下PowerMockito
的mockStatic
的另一种用法了:
//第一个参数为要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多平台发布
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。