概述
单测是提升软件质量的有力手段。然而,由于编程语言上的支持不力,以及一些不好的编程习惯,导致编写单测很困难。
最容易理解最容易编写的单测,莫过于独立函数的单测。所谓独立函数,就是只依赖于传入的参数,不修改任何外部状态的函数。指定输入,就能确定地输出相应的结果。运行任意次,都是一样的。在函数式编程中,有一个特别的术语:“引用透明性”,也就是说,可以使用函数的返回值彻底地替代函数调用本身。独立函数常见于工具类及工具方法。
不过,现实常常没有这么美好。应用要读取外部配置,要依赖外部服务获取数据进行处理等,导致应用似乎无法单纯地“通过固定输入得到固定输出”。实际上,有两种方法可以尽可能隔离外部依赖,使得依赖于外部环境的对象方法回归“独立函数”的原味。
(1) 引用外部变量的函数, 将外部变量转化为函数参数; 修改外部变量的函数,将外部变量转化为返回值或返回对象的属性。
(2) 借助函数接口以及lambda表达式,隔离外部服务。
隔离依赖配置
先看一段代码。这段代码通过Spring读取已有服务器列表配置,并随机选取一个作为上传服务器。
public class FileService { // ... @Value("${file.server}") private String fileServer; /** * 随机选取上传服务器 * @return 上传服务器URL */ private String pickUrl(){ String urlStr = fileServer; String[] urlArr = urlStr.split(","); int idx = rand.nextInt(2); return urlArr[idx].trim(); } }
咋一看,这段代码也没什么不对。可是,当编写单测的时候,就尴尬了。 这段代码引用了实例类FileService的实例变量 fileServer ,而这个是从配置文件读取的。要编写单测,得模拟整个应用启动,将相应的配置读取进去。可是,这段代码无非就是从列表随机选取服务器而已,并不需要涉及这么复杂的过程。这就是导致编写单测困难的原因之一:轻率地引用外部实例变量或状态,使得本来纯粹的函数或方法变得不那么“纯粹”了。
要更容易地编写单测,就要尽可能消除函数中引用的外部变量,将其转化为函数参数。进一步地,这个方法实际上跟 FileService 没什么瓜葛,反倒更像是随机工具方法。应该写在 RandomUtil 里,而不是 FileService。 以下代码显示了改造后的结果:
public class RandomUtil { private RandomUtil() {} private static Random rand = new Random(47); public static String getRandomServer(String servers) { if (StringUtils.isBlank(servers)) { throw new ExportException("No server configurated."); } String[] urlArr = servers.split(","); int idx = rand.nextInt(2); return urlArr[idx].trim(); } } private String pickUrl(){ return RandomUtil.getRandomServer(fileServer); }
public class RandomUtilTest { @Test public void testGetRandomServer() { try { RandomUtil.getRandomServer(""); fail("Not Throw Exception"); } catch (ExportException ee) { Assert.assertEquals("No server configurated.", ee.getMessage()); } String servers = "uploadServer1,uploadServer2"; Set<String> serverSet = new HashSet<>(Arrays.asList("uploadServer1", "uploadServer2")); for (int i=0; i<100;i++) { String server = RandomUtil.getRandomServer(servers); Assert.assertTrue(serverSet.contains(server)); } } }
这样的代码并不鲜见。 引用实例类中的实例变量或状态,是面向对象编程中的常见做法。然而,尽管面向对象是一种优秀的宏观工程理念,在代码处理上,却不够细致。而我们只要尽可能将引用实例变量的方法变成含实例变量参数的方法,就能让单测更容易编写。
隔离依赖服务
一个分页例子
先看代码。这是一段很常见的分页代码。根据一个查询条件,获取对象列表和总数,返回给前端。
@RequestMapping(value = "/searchForSelect") @ResponseBody public Map<String, Object> searchForSelect(@RequestParam(value = "k", required = false) String title, @RequestParam(value = "page", defaultValue = "1") Integer page, @RequestParam(value = "rows", defaultValue = "10") Integer pageSize) { CreativeQuery query = new CreativeQuery(); query.setTitle(title); query.setPageNum(page); query.setPageSize(pageSize); List<CreativeDO> creativeDTOs = creativeService.search(query); Integer total = creativeService.count(query); Map<String, Object> map = new HashMap<String, Object>(); map.put("rows", (null == creativeDTOs) ? new ArrayList<CreativeDO>() : creativeDTOs);