代码的单元测试非常重要,是团队开发中必不可少的一环!这个懂的人,应该无需赘言了吧!!!(重要的事情说三遍)
那如何才能写好单元测试呢?网上提到过很多优秀的项目单元测试必须满足的原则,诸如覆盖率,完整性,自动化等等。今天我想从为什么写不好单元测试,甚至是无法写出单元测试的理由入手,给出一些个人的建议与方法,希望能给大家一些启发和思考。
为什么写不好单元测试呢?很多人给出的理由是,系统的耦合性太高了,为了测试一个接口,需要考虑太多东西了。以我们项目的实际为例,业务层的接口实现调用了事务控制层的接口,然后事务控制层的接口实现又调用了DAO层的接口,一层扣一层,真是不好写啊!!
要写好这类代码的单元测试,普世的原则自然是解耦了。想必道理大家都懂,但是真正做起来就很麻烦了。其实,我的总结是,各层既然有明确的分工和职责,那单元测试关注的点应该也是自身的职责所在。还是以我们的项目为例,业务层的职责是面向过程的代码过程,每个接口实现的是业务流转和逻辑,业务层并不关心数据怎么存储和读取(这不应该是业务层应该关心的)。业务层的单元测试也应该服务于此,比如一个保存的接口,我们只需要测试其要保存的对象数据是否完整,校验规则是否能照顾到。有人可能问,如果这个接口有返回值,当然好验证了,那没有返回值的情况怎么验证呢?这个时候我们就需要用到mock的思想了。
话不多说,上点代码吧。
要测试业务层这个接口如下:
public RemoteResult<Boolean> savePushContent(final PushContentApi pushContentApi) {
return RemoteServiceInvoker.doRunWithExHandler(new RemoteObject<Boolean>() {
@Override
public RemoteResult<Boolean> run() {
RemoteResult<Boolean> result = new RemoteResult<Boolean>(true);
pushContentApi.validateAddBean();
if (TYPE_FASTPUSH.equals(pushContentApi.getType())) {
pushContentApi.setSerialNo(SERIALNOFAST_PRE + IdGenerator.genSerialNoWithDateAndRandom2());
} else {
pushContentApi.setSerialNo(SERIALNO_PRE + IdGenerator.genSerialNoWithDateAndRandom2());
}
pushContentApi.setSendState(SEND_STATE_WAIT_CONFIRM);
setSendInfo(pushContentApi);
pushContentManager.savePushContent(BeanCopyUtil.createAndCopy(pushContentApi, PushContent.class));
result.setT(true);
return result;
}
});
}
代码里调用了pushContentManager的接口。我们mock一下这个manager,然后实现一下savePushContent一下。
@Override
public void savePushContent(PushContent pushContent) {
assertNotNull(pushContent);
assertNotNull(pushContent.getSerialNo());
assertEquals("测试内容", pushContent.getContent());
assertEquals(Long.valueOf(-1), pushContent.getCreatorId());
assertEquals(PushContentConstants.SEND_STATE_WAIT_CONFIRM, pushContent.getSendState());
if (pushContent.getTaskId() == null) {
assertEquals("13810001000,13810001001", pushContent.getTestMobiles());
assertEquals(Long.valueOf(2), pushContent.getSendNum());
} else if (MockDataTaskManager.DATA_TASK_ID.equals(pushContent.getTaskId())) {
assertEquals(Long.valueOf(201), pushContent.getActivityId());
assertEquals(Long.valueOf(100), pushContent.getSendNum());
}
}
然后单元测试如下:
@Test
public void testSavePushContent() throws Exception {
// 测试关联数据任务的内容
PushContentApi pushContentApi = new PushContentApi();
pushContentApi.setContent("测试内容");
pushContentApi.setActivityId(201l);
pushContentApi.setCreateTime(new Date());
pushContentApi.setCreatorId(-1l);
pushContentApi.setCreatorName("测试用户");
pushContentApi.setTaskId(MockDataTaskManager.DATA_TASK_ID);
pushContentApi.setUpdateTime(new Date());
pushContentApi.setUpdatorId(-1l);
pushContentApi.setUpdatorName("测试用户");
pushContentSoaService.savePushContent(pushContentApi);
// 测试不关联数据任务的内容
PushContentApi pushContentApi1 = new PushContentApi();
pushContentApi1.setContent("测试内容");
pushContentApi1.setCreateTime(new Date());
pushContentApi1.setCreatorId(-1l);
pushContentApi1.setCreatorName("测试用户");
pushContentApi1.setTestMobiles("13810001000,13810001001");
pushContentApi1.setUpdateTime(new Date());
pushContentApi1.setUpdatorId(-1l);
pushContentApi1.setUpdatorName("测试用户");
pushContentSoaService.savePushContent(pushContentApi1);
}
验证的逻辑都放在了mock出来的manager的接口实现里,跑一下这个单元测试,就能验证正确的逻辑了。这个只是很简单的代码思路,业务层接口实现里如果有很多校验规则与逻辑的话,还需要分别测试。如果要测试真实的manager接口实现,那又再单独写他的单元测试了,但是应该是与业务层的单元测试完全解耦的。
总结一下,单元测试要想写好,必须要灵活运用解构技巧,关注点始终放在各层的分工与职责上。而且有了这种思想,就能实现真正的测试驱动开发了(测试用例先行,你懂的)。