作者 | 邱庆羽(羽哲)
来源 | 阿里开发者公众号
前言
在去年的时候就读过《重构:改善既有代码的设计》这本在代码重构领域里的经典,当时在读的时候就苦于有这么两点导致只停留在了“读”上面,而缺少实践。
1.全书内容比较枯燥乏味,虽然有部分代码示例,但是语言并不生动,让人坚持读下去就很难。
2.书中强调,每做一次代码的重构,即使修改很小的部分,也应该对修改进行测试,所以在看到书中举例的各种重构和重构后的不断测试时,就对测试和重构打起了退堂鼓
因为对代码的测试缺少整体的认知,对单元测试也一直停留在简单的print层面,直到在这几天通过一个阅读打卡活动,系统性的学习了大量关于单元测试的理论、实践知识和在代码重构中的帮助,重新让我回忆起《重构》书中所提到的知识点,一直跃跃欲试,所以也想尝试将单元测试实际应用在自己写的业务代码中,为自己的代码“保驾护航”,同时在代码中有单测,在重构的过程中单测也会让自己不慌张,修改代码后直接执行一下单测就可以对修改后的准确性进行验证。这篇文章着重在“实践”上,是对Java编程技巧之单元测试用例编写流程这篇文章的实际应用,并没有高深的理论和技术。
流程
包括单元测试在内的所有测试都应该对所需要测试的流程有比较清晰的认识才能保证测试的用例不被遗漏,保证测试质量,因此在撰写单元测试前,首先需要对所测试部分代码的流程进行梳理。
1.用户发起查询时,首先判断用户传入的参数中是否包含行权验证的字段,如果不包含则直接抛出异常,提示鉴权失败;
2.在鉴权中,因为行权可能会由多个字段组合鉴权,因此需要对全部鉴权字段进行遍历。在鉴权时,还会有匹配字段,包含:
- 等值匹配(=)
- 前缀匹配(like xxx%)
- 后缀匹配(like %xxx)
- 模糊匹配(like %xxx%)
四种形式,当用户输入参数与匹配字段不相符时,记作鉴权失败。
3.当用户没有输入参数中的权限时,记作鉴权失败。
4.在多个鉴权字段进行组合时,还会存在两种组合逻辑:and(所有字段均需要有权限才能访问)和 or(任意字段有权限即可访问),所以还会结合组合逻辑进行综合权限判定。
测试边界
在明确了代码的流程后,就需要对测试边界进行明确,测试应该着重对边界情况进行测试,因为清晰的单元测试边界划分有利于构建更加稳定的系统核心代码,因为我们在推进测试边界的过程中会不断地将副作用从核心代码中剥离出去,最终会得到一个完整且可测试的核心,而我们的核心代码就是处于测试边界内的。下图将所有的情况做了枚举,作为单元测试的用例。
撰写单元测试
引入依赖
<dependency> <groupId>org.powermock</groupId> <artifactId>powermock-core</artifactId> <version>${powermock.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito2</artifactId> <version>${powermock.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>${powermock.version}</version> <scope>test</scope> </dependency>
其中,powermock.version为2.0.9,为当前的最新版本,可根据实际情况修改。在PowerMock包中,已经包含了对应的Mockito和JUnit包,所以无需单独引入Mockito和JUnit包。
模拟方法
1.首先是需要对外部依赖和进行测试的类进行模拟和注入:
// 需要模拟的外部依赖 @Mock private ArkDataSetReadService arkDataSetReadService; // 需要进行测试的类 @InjectMocks private ComponentJobExecuteService componentJobExecuteService;
2.之后就是对依赖的方法进行模拟:
Response<RowPermissionQueryResponseDto> response = JSON.parseObject(permissionData, Response.class); Mockito.doReturn(response).when(arkDataSetReadService).getUserRowPermissions(911L, 1741141);
●通过文本的方式构造模拟的返回值。
●通过Mockito.doReturn() 方法模拟调用时的返回值。
3.最后就是对所测试的方法进行测试:
Map<String, Pair<String, List<String>>> filterParams = new HashMap<>(); Whitebox.invokeMethod(componentJobExecuteService, "checkRowPermission", 911L, filterParams)
完整的测试代码:
@Test(expected = AuthException.class) public void testRowPermission() throws Exception { ContextInfo contextInfo = new ContextInfo(); contextInfo.setBucId(1741141); ContextInfoHolder.setContextInfo(contextInfo); Response<RowPermissionQueryResponseDto> response = JSON.parseObject(permissionData, Response.class); Mockito.doReturn(response).when(arkDataSetReadService).getUserRowPermissions(911L, 1741141); Map<String, Pair<String, List<String>>> filterParams = new HashMap<>(); Whitebox.invokeMethod(componentJobExecuteService, "checkRowPermission", 911L, filterParams); }
因为在没有权限的时候会直接抛出 AuthException,所以通过@Test(expected = AuthException.class) 进行验证。
到此,行权验证的单元测试框架就初步搭建完成,接下来就是根据前面梳理的单元测试用例的边界构建不同的参数进行验证。
总结
在实际撰写单元测试时,由于对工具的不熟悉担心写起来比较麻烦,所以一直没进行尝试,但是在实际操作的时候发现,在写单元测试的时候,其实也是对整个代码进行梳理的过程,是对自己写代码时思路的复盘和总结,单测不只是能验证代码执行的准确性,还能验证在写代码时的逻辑是否正确、能否满足业务的实际需求。
有了单测打底,接下来即使需要对这部分代码进行重构、迭代,也能做到心中有数,只需要在重构完成后执行下单测就可以轻松验证代码的准确性。
希望能够通过这次实践,能够让自己在写单测的路上坚持下去。