可维护性障碍之一:重复
那么什么是重复?它对我们意味着什么?简单地说,重复是存在多份拷贝或对单一概念的多次表达。这都是不必要的重复。最明显的重复也许就是某一个数字值或字符串在代码中反复出现。有时重复不仅存在于散落各处的硬编码中,还出现在两个类、对象或方法之间的代码逻辑片段和重叠的职责之内。
重复是不好的,它增加了代码的不透明性,使散落在各处的概念和逻辑很难理解。此外,对于修改代码的程序员来说,每一处重复都是额外的开销,如果忘记在所有必要的地方都加以改动,又增加了出现bug的机会。
破解之道:通过提炼变量和方法,消除重复。
代码示例见《有效的单元测试》。
可维护性障碍之二:条件逻辑
假设我们正在重构并运行测试来确保一切正常,此时我们发现某个测试失败了。这真是出乎意料,我们没想到这点儿变更却会影响测试,但它的确发生了。我们从失败的测试开始检查堆栈跟踪,却发现自己无法知道失败的测试当时在干什么。
条件逻辑障碍示例:
publicclassDictionaryTest{
TestpublicvoidreturnsAnIteratorForContents()throwsException{
Dictionarydict=newDictionary();
dict.add(A,newlong(3));
dict.add(B,21);
for(Iteratore=dict.iterator();e.hasNext();{
Map.Entryentry=(Map.Entry)e.next();
if(A.equals(entry.getKey())){
assertEuqals(3L,entry.getValue());
}
}
}
}
破解之道:这种情况在处理过度复杂的代码时经常遇到,第一步就是简化它,通常是将一段炼到独立且命名良好的方法中。
改进后的代码示例:
TestpublicvoidreturnsAnIteratorForContents()throwsException{
Dictionarydict=newDictionary();
dict.add(A,newlong(3));
dict.add(B,21);
assertContains(dict.iterator(),A,3L);
assertContains(dict.iterator(),B,21);
}
privatevoidassertContains(Iteratori,Objectkey,
objectvalue){
while(i.hasNext()){
Map.Entryentry=(Map.Entry)i.next();
if(key.equals(entry.getKey())){
assertEuqals(value,entry.getValue());
return;
}
}
fail(Iteratordidntcontain+key+=+value);
}
可维护性障碍之三:脆弱测试
也就是说,虽然测试使构建失败了,但是你去看的时候,再次执行却又通过了,刚才似乎只是个意外。
脆弱测试失败时都涉及了线程和竞态条件,其行为依赖于日期或时间,或测试依赖于计算机的性能,因而在运行期间受到I/O速度或CPU的影响。
对于间歇性的测试失败,时间戳并非唯一的麻烦来源。最突出的是多线程,它往往会使事情复杂化。涉及随机数生成的也好不到哪儿去。有些情况更为明显,有些则没那么明显真是防不胜防。
破解之道:
无论我们是否提早意识到要针对随机测试失败来保护自己,我们解决这些博况的方法都是一样的:
1.规避它。
2.控制它.
3.隔离它.
最简单的办法就是彻底地规避这个问题,切断任何不确定行为的来源。例如,我们可以通过文件名的数字后缀而非时间戳来对日志文件排序。
如果不能轻易地绕过棘手的部分我们就试图控制它。例如,可以采用fake(伪造对象)来代替随机数生成器,令其精确地返回我们需要的值。
最后如果找不到一个方法来充分地规避或控制这个问题的源头,我们的最后―招就是将难题隔离在尽可能小的范围内。这就将大多数代码从不确定行为中解脱出来,从而在一处而且仅仅一处来解决难题。
代码示例见《有效的单元测试》。
可维护性障碍之四:残缺的文件路径
残缺的文件路径会使你无法转移代码,除了自己的计算机之外,在任何其他人的计算机上都无法运行。我们都知道这个问题。
破解之道:
当你在测试代码中处理文件时,应该避免绝对路径。当然也会有可以接受的例外情况,但作为一条经验之谈,你还是尽量避开它们吧。相反,尽量采用抽象引用,比如系统属性(Systemproperty)、环境变量,或任何能获得的相对路径。
代码示例见《有效的单元测试》。
可维护性障碍之五:永久的临时文件
临时文件应该是暂时性的,使用后即抛弃和删除。永久的临时文件这个测试坏味道,指的是临时文件并非暂存而是被保留了下来。也就是说在下个测试运行前它都不会被删除。
破解之道:
一般来说,能不用物理文件就尽量不用;比起操作字符串或其他类型的内存块来说,文件I/O会使得测试变慢。说到这,当你处理测试中的临时文件时,你需要记住临时文件并不是随时保持临时性的。
针对临时文件的处理,我们归纳出几条简单的指导方针:
1.在
Before中删除文件。2.如果可能的话,使用唯一的临时文件名。
3.要弄清楚文件是否应该存在.
代码示例见《有效的单元测试》。
可维护性障碍之六:沉睡的蜗牛
比起处理内存中的数据,使用文件I/O是极其缓慢的。再说一遍,缓慢的测试是影响可维护性的主要障碍,因为不论是添加新功能还是修复问题,程序员在修改代码时都需要反复地运行测试。
文件I/O并非缓慢测试的唯一来源。通常对测试套件执行时间带来的更大冲击是源自大量的Thread#sleep调用,它允许其他线程先完成工作,然后再对预期结果和副作用进行断言。沉睡的蜗牛非常容易发现:查找Thread#sleep调用以及留意异常缓慢的测试。
破解之道:
一种更好的方式,替换掉了奢侈和不可靠的Thread#sleep。最根本的不同在于我们使用了同步对象,使得测试能够立即知晓工作线程的任务结束了。
代码示例见《有效的单元测试》。
可维护性障碍之七:像素完美
像素完美是基本断言和魔法数字的特例。把它放在这个坏味道分类中,是因为我觉得它在计算机绘图领域频繁出现,那里的人们经常会说在测试中遇到困难。
基本上,像素完美坏味道出现的场景包括,测试要求期望和实际产生的图像完美地匹配,或者期望在产生的图像中,指定坐标处的像素包含某种颜色。这样的测试是出了名的脆弱,通常很小的输入变化都会导致测试失败。
破解之道:
用适当的抽象层次来表达测试,这是编写它们的目标之一。这就是说不要用基本断言,而是应该将背后的细枝末节隐藏到自定义断言中,那里才是适合的抽象层次。同时也意味着,相对于精确的值与值比较,断言中反而需要进行模糊匹配——一种实际的算法。
代码示例见《有效的单元测试》。
可维护性障碍之八:参数化混乱
参数化测试是一种良好的模式,但是在错误的上下文中过于急切地使用,参数化测试就变成了一种坏味道——参数化混乱,它难以理解,而且当某个测试失败时难以定位实际错误。
破解之道:
你可能听过―个人对医生抱怨的笑话“这样动我的胳膊会疼”。医生回答说,“那就别动”。这也是针对参数化混乱的最简单的解决方案。有鉴于此,而且看到这么多参数化测试最终带来的只是白头发和办公室中的俏皮话,那么采用参数化测试模式就要三思而后行。
当你决定采用参数化测试模式时,你可以做一些简单的事情来避免阅读的数据,以及难以识别的匿名测试失败。首先哪家治疗白癜风效果最好哪家白癜风医院权威