如何写好单元测试
代码的单元测试非常重要,是团队开发中必不可少的一环!这个懂的人,应该无需赘言了吧!!!(重要的事情说三遍)
那如何才能写好单元测试呢?网上提到过很多优秀的项目单元测试必须满足的原则,诸如覆盖率,完整性,自动化等等。今天我想从为什么写不好单元测试,甚至是无法写出单元测试的理由入手,给出一些个人的建议与方法,希望能给大家一些启发和思考。
为什么写不好单元测试呢?很多人给出的理由是,系统的耦合性太高了,为了测试一个接口,需要考虑太多东西了。以我们项目的实际为例,业务层的接口实现调用了事务控制层的接口,然后事务控制层的接口实现又调用了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接口实现,那又再单独写他的单元测试了,但是应该是与业务层的单元测试完全解耦的。
总结一下,单元测试要想写好,必须要灵活运用解构技巧,关注点始终放在各层的分工与职责上。而且有了这种思想,就能实现真正的测试驱动开发了(测试用例先行,你懂的)。