购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.2 代码规范与单元测试

2017年,阿里巴巴发布编码规范,这是开源界的一件大事,也在知乎等平台上引发了广泛的讨论,其中有个别回复纠结于具体细节的商榷和建议,但大部分认同该规范的指导意义。本节拟从代码规范、单元测试、代码审查及审查清单谈谈笔者的一些体会。

1.2.1 编码规范

不以规矩,不能成方圆。为什么要有规范(规约)想必不用多说了。本书重在讲述程序员如何具备大局观,具备大局观所要涵盖的知识宽度和视野,因此对于具体编码规范的逐条解析不作为重点。

Google Java Style Guide包含的内容有源文件基础、源文件结构、格式、命名、编程实践和 Javadoc,可以作为一个团队必须遵守的共识。一套好的规范应该搭配好的审查清单(CheckList)。《Java 开发手册 1.5.0》就是一个不错的融合规约和审查清单的案例,其中的单元测试一章有一条规范检查项,引用如下:

【强制】 单元测试应该是全自动执行的,并且是非交互式的。测试框架通常是定期执行的,执行过程必须完全自动化才有意义。对输出结果需要人工检查的测试不是好的单元测试。在单元测试中不准使用System.out进行人肉验证,必须使用Assert进行验证。

该项可以作为一条独立的审查清单纳入CheckStyle或者PMD这样的工具来扫描静态代码。对于单元测试应该如何编写,会在下面的单元测试小节展开讨论。

1.2.2 单元测试

单元测试(Unit Testing,UT)说起来简单,在实际操作过程中却要注意它不是为了测试而测试的。如下所示是一段对Java中的字符串进行右对齐操作的代码:

测试代码如下:

如何采用测试驱动设计(Test-Driven Development,TDD)方式写这段业务代码呢?这里先看一下要满足的需求:按照指定的目标长度扩展并右对齐字符串,用指定的字符串填充目标字符串的左边。

所以,这个需求对应的测试用例如下。

◎ 如果源字符串为null,则无论指定目标长度为多少,结果都为null。

◎ 如果源字符串为""(空串),指定目标长度为3、填充字符串为"w",则结果为"www"。

◎ 如果源字符串为"bat",指定目标长度为3、填充字符串为"yw",则结果为"bat"。

◎ 如果源字符串为"bat",指定目标长度为5、填充字符串为"yw",则结果为"ywbat"。

◎ 如果源字符串为"bat",指定目标长度为1、填充字符串为"yw",则结果为"bat"。

◎ 如果源字符串为"bat",指定目标长度为-1、填充字符串为"yw",则结果为"bat"。

1.2.3 测试驱动设计

笔者在 2010 年做了一件现在看起来不靠谱的事情,就是把一个模块的代码测试覆盖率做到了80%,在笔者刚接手时其覆盖率是30%。为此,笔者让一位研发人员补测试补了一周,这可以算作为了覆盖率而做,但其效果如何,我们不敢抱太大希望,比如在下一次重构的时候,这些测试代码是否值得信赖。

有位咨询师提到,测试覆盖率低的病灶常常如下。

◎ 团队成员没有写测试的习惯,没有意识到写测试的重要性,不想写。

◎ 代码难于测试,不会写。

◎ 赶进度,没有时间写。

相对于提升测试的覆盖率,解决这些问题要复杂、棘手得多。笔者不确定上述问题在特定环境下的解决方法是什么,但很确定补测试不是良方,它往往会催生出没有 Assert的畸形测试及大量针对getter、setter的无用测试。

这件事情给笔者一个教训,就是单元测试一定不能事后补。我们团队在项目实际操作过程中没有严格遵循测试驱动设计的步骤,但是遵循了同步编写开发代码和测试代码的思路。

测试驱动设计的基本思想就是在开发功能代码之前先编写测试代码,也就是说在明确要开发的需求之后,首先思考如何分析这个需求,并完成测试代码的编写,然后编写相关代码来满足这些测试用例,最后循环添加其他测试用例,直到该需求对应的测试用例都测试通过。

测试驱动设计有3个原则,如下所述。

◎ 原则1:无测试,不代码。

◎ 原则2:单元测试不在多,能够识别出问题即可。

◎ 原则3:代码不在多,让当前单元测试全部通过即可。

下面看看具体的操作步骤,以1.2.2节的需求为例。

第1步:金丝雀测试

“金丝雀测试”的概念来自早期的煤炭矿井行业:金丝雀对有毒气体比较敏感,在 19世纪左右,英国的矿井工人在下矿井时常常会带一只金丝雀,如果矿井内的有毒气体超标,金丝雀就会立刻死亡,这会救矿井工人一命。

同样,在测试驱动设计实践中,在开发具体的测试用例之前,也需要先写一个dummy的测试用例,确保整个编译、运行和JUnit环境是正常运行的。

之后运行这个测试用例,结果符合预期,说明整个编译、执行和JUnit环境是好的。

第2步:编写第1个最简单的单元测试

对应的需求为:如果源字符串为null,则无论指定的目标长度为多少,结果都为null。编写测试代码如下:

好,在第1个单元测试运行时出现红色,说明编译出现了问题。开始编写如下代码:

这样,第1个单元测试在运行时就是绿色的了。也许有人会问:“这段代码有用吗?”然而,从另一方面来说,这不就是实现当前需求的最简洁和最高效的实现吗?

第3步:编写第2个单元测试

对应的需求为:如果源字符串为""(空串),指定目标长度为 2、填充字符串为"w",则结果为"ww"。编写单元测试,代码如下:

测试时再次出现红色,将代码修改如下:

这段代码非常具体(Specific),没有通用性(Generic),但是不妨碍非常高效地满足了当前的需求。

第4步:继续加需求

对应的需求为:如果源字符串为""(空串),指定目标长度为 3、填充字符串为"w",则结果为"ww"。

这个需求其实是和上一个需求等价的,所以可以把这个单元测试和上一个单元测试合并:

再运行一次,结果怎样呢?运行结果正确!

第5步:接着加需求

对应的需求为:如果源字符串为"abc",指定目标长度为 3、填充字符串为"w",则结果为"abc"。

增加测试用例,代码如下:

运行一次单元测试,发现运行失败。看来我们要继续写代码了,再加入一个if块:

第6步:再次加需求

对应的需求为:如果源字符串为"abc",指定目标长度为5、填充字符串为"wxy",则结果为"wxabc"。相关测试代码如下:

实现代码如下:

第7步:重构,是为了更好地前行

到目前为止,我们的代码已经“生长”到 30 行了,现在选择重构当前代码,主要关注两方面:让代码更整洁;让应用“从特殊到一般”来泛化代码,使“算法”更清晰。

首先,为了提取“pattern”,我们发现第1个if块和第3个if块有些类似。为了让它们呈现一样的“pattern”,我们在第1个if块中加入一条dummy语句:

然后,为了发掘pattern,对下面的代码块进行重构:

这段代码的逻辑是,当输入的源字符串长度为零,而填充字符的长度为1时,重复利用填充字符进行填充。我们可以重构“重复利用填充字符进行填充”这段代码:

这样的话,就可以把这个if块重构为和其他if块相似的pattern:

这样一来,就可以通过合并第1个if块和第3个if块进行重构:

重构的结果如下:

第8步:继续加入需求

对应的需求为:如果源字符串为"abc",指定目标长度为 1、填充字符串为"wxy",则结果为"abc"。相关测试代码如下:

运行单元测试,结果居然是通过!

第9步:继续加入需求

对应的需求为:如果源字符串为"abc",指定目标长度为-1、填充字符串为"wxy",则结果为"abc",也就是说不进行处理。相关测试代码如下:

运行单元测试,结果仍然是通过。

也就是说,在前面的重构中,在使用了“从特殊到一般”对代码进行泛化重构之后,代码的使用范围更广了,或者说隐藏在背后的算法显现了,这也说明测试驱动设计是可以演化出算法的。 13rz0SbiFST1sES362qGMECE7vvHlcrXsNldvFttp9gblEnUkF1PB8nR17ki+9QZ

点击中间区域
呼出菜单
上一章
目录
下一章
×