本文举例本人参与项目其中的一个业务接口:消费券删除,说明了如何使用Mock打桩编写单元测试,以及需要注意的点。
需要做单元测试的Service层业务代码:
public ResultBean deleteById(String id, String userId) {
WxCouponInfo wxCouponInfo = this.wxCouponInfoMapper.selectByPrimaryKey(id);
if (wxCouponInfo == null || wxCouponInfo.getStatus() == CommonConstants.MODEL_STATUS_DELETE) {
return ResultBean.error("消费券已被删除,操作失败");
}
if (0 < wxCouponInfo.getActualTotalNum()) {
return ResultBean.error("已有领取记录,无法删除");
}
List<String> activityNameList = this.wxCouponActivityRelatedCouponMapper.getCountByCouponInfoId(id);
if(null != activityNameList && activityNameList.size() > 0){
return ResultBean.error("消费券已挂在"+activityNameList.toString()+ "活动上,请先在对应活动里下架该券再删除");
}
wxCouponInfo.setStatus(CommonConstants.MODEL_STATUS_DELETE);
wxCouponInfo.setUpdateUserId(userId);
wxCouponInfo.setUpdateTime(new Date());
this.wxCouponInfoMapper.updateByPrimaryKey(wxCouponInfo);
//删除活动关联消费券表记录
this.wxCouponActivityRelatedCouponMapper.updByCouponInfoId(id);
return ResultBean.ok("操作成功");
}
为了保证单元测试的覆盖率,一般情况下if语句里若是直接返回错误信息,这条路径是不用单独编写测试代码的,只用测试正常返回情况的有效代码,因为如果走了错误信息的分支的话,那么单元测试就直接return跳出方法了,下面的大部分有效代码都覆盖不到,这个单元测试就没有意义了。在这个方法里,以下分支是不用写测试代码走的:
if (wxCouponInfo == null || wxCouponInfo.getStatus() == CommonConstants.MODEL_STATUS_DELETE) {
return ResultBean.error("消费券已被删除,操作失败");
}
if (0 < wxCouponInfo.getActualTotalNum()) {
return ResultBean.error("已有领取记录,无法删除");
}
if(null != activityNameList && activityNameList.size() > 0){
return ResultBean.error("消费券已挂在"+activityNameList.toString()+ "活动上,请先在对应活动里下架该券再删除");
}
想要单元测试不走这三条分支的话,那么就要设置与if语句的条件相反的值使其跳过这些分支,比如说第一个if语句,括号里的判断条件是:
wxCouponInfo为null 或 wxCouponInfo.getStatus()为CommonConstants.MODEL_STATUS_DELETE
在打桩的时候, wxCouponInfo的值就要设置不为空,并且wxCouponInfo.getStatus()的值不能为CommonConstants.MODEL_STATUS_DELETE(一个提前设置好的静态变量),这样单元测试代码就会跳过这条分支,继续扫描下面的代码。按照这个思路,方法中的分支或者代码块都可以通过设置打桩的值来控制是走还是跳过。
在对Service层做单元测试时,主要需要测试的代码就是涉及到数据库操作以及调用其他方法的部分,由于示例代码只涉及到数据库操作,所以主要说明要怎么对数据库操作做测试,调用其他方法的测试就稍微复杂点,需要对被调用方法内部的代码也进行打桩,其实就是一个方法中嵌套了另一个方法,这两个方法的代码都会被单元测试扫描到,所以需要当做一个整体来写单元测试。
示例代码的单元测试如下:
public class WxCouponStockBizTest {
//桩注入
@Mock
private static WxCouponInfoMapper wxCouponInfoMapper;
@Mock
private static WxCouponActivityRelatedCouponMapper wxCouponActivityRelatedCouponMapper;
@InjectMocks
private static WechatCouponStockBiz wechatCouponStockBiz;
private static Date date;
private static String uid;
private static WxCouponInfo wxCouponInfo;
static {
uid = "123";
wxCouponInfo = new WxCouponInfo();
wxCouponInfo.setId("123");
wxCouponInfo.setWechatMchid("123");
wxCouponInfo.setWechatMchName("123");
wxCouponInfo.setCouponStockId("123");
wxCouponInfo.setActualTotalNum(0);
wxCouponInfo.setPreDailyNum(123);
wxCouponInfo.setServiceTarget(2);
wxCouponInfo.setGrandStartTime(date);
wxCouponInfo.setGrandEndTime(date);
wxCouponInfo.setPerUserDailyNum(123);
wxCouponInfo.setWriteOffNum(123);
wxCouponInfo.setCreateUserId("123");
wxCouponInfo.setCreateTime(date);
wxCouponInfo.setStatus(0);
@Before
public void setUp() throws Exception {
// 初始化mock对象
MockitoAnnotations.initMocks(this);
// 开始mock静态打桩
PowerMockito.mockStatic(Date.class);
PowerMockito.whenNew(Date.class).withNoArguments().thenReturn(date);
PowerMockito.mockStatic(IDUtils.class);
PowerMockito.when(IDUtils.nextId()).thenReturn("123");
}
@Test
public void deleteByIdTest() throws Exception {
List<String> list = new ArrayList<>();
when(wxCouponInfoMapper.selectByPrimaryKey(uid)).thenReturn(wxCouponInfo);
when(wxCouponActivityRelatedCouponMapper.getCountByCouponInfoId(uid)).thenReturn(list);
when(wxCouponInfoMapper.selectByPrimaryKey("123")).thenReturn(wxCouponInfo);
List<String> activityNameList = new ArrayList<String>();
when(wxCouponActivityRelatedCouponMapper.getCountByCouponInfoId("123")).thenReturn(activityNameList);
when(wxCouponActivityRelatedCouponMapper.updByCouponInfoId("123")).thenReturn(1);
wxCouponInfo.setUpdateUserId(uid);
wxCouponInfo.setUpdateTime(date);
when(wxCouponInfoMapper.updateByPrimaryKey(wxCouponInfo)).thenReturn(1);
when(wxCouponActivityRelatedCouponMapper.updByCouponInfoId(uid)).thenReturn(1);
ResultBean result = wechatCouponStockBiz.deleteById(uid,uid);
assertEquals(1, result.get("code"));
}
}
需要注意的一点是,单元测试必须先打好所有桩,最后才调用要测试的方法,意思是以下这句代码
ResultBean result = wechatCouponStockBiz.deleteById(uid,uid);
必须写在所有桩都打好之后,最后一步,才是调用。
有一种比较难处理的情况是,if语句里面并不是返回错误信息的代码,而是有涉及到数据库操作或者是调用其他方法的有效分支,这种如果想保证覆盖率的话,可能就需要设置几种不同的值,多次调用方法,来使每个分支都走到,结果会导致单元测试代码比业务代码多得多,特别是在代码逻辑比较多而且复杂的情况下,所以说还是要衡量一下,每个分支都走到是否有必要,适当做取舍。