本書第二部分中主要介紹了可讀性/可維護(hù)性/可靠性三個(gè)方面的code smell或者說(shuō)反模式(anti-pattern)绩社。
我們首先看可讀性的code smell. 再次解釋一下所謂可讀性炸宵,就是代碼的意圖和行為能通過(guò)閱讀代碼就能得到準(zhǔn)確而清晰的表達(dá)猾愿。
- 原始的斷言 (Primitive assertions)
意思是assert所做的判斷抽象層級(jí)過(guò)于低隘马,是在測(cè)試代碼的實(shí)現(xiàn)。如書中下面的例子:
@Test
public void outputHasLineNumbers() {
String content = "1st match on #1\nand\n2nd match on #3";
String out = grep.grep("match", "test.txt", content);
assertTrue(out.indexOf("test.txt:1 1st match") != -1);
assertTrue(out.indexOf("test.txt:3 2nd match") != -1);
}
其中的indexOf 和 magic number -1 都過(guò)于和java的String類的實(shí)現(xiàn)相關(guān)了。
修改后的版本用到了org.junit.JUnitMatchers#containsString()方法,對(duì)比之前犀斋,是不是可讀性好了很多呢?
@Test
public void outputHasLineNumbers() {
String content = "1st match on #1\nand\n2nd match on #3";
String out = grep.grep("match", "test.txt", content);
assertThat(out, containsString("test.txt:1 1st match"));
assertThat(out, containsString("test.txt:3 2nd match"));
}
我們應(yīng)該追求測(cè)試代碼的本質(zhì)情连,也就是正確的行為叽粹,而不是去測(cè)試代碼的實(shí)現(xiàn)細(xì)節(jié)。
超斷言(Hyperassertions)
這是指斷言的內(nèi)容過(guò)于具體和瑣碎却舀,或者過(guò)于脆弱虫几,非本質(zhì)的改變就會(huì)影響測(cè)試結(jié)果⊥彀危或者說(shuō)我們所測(cè)試的輸出過(guò)于龐大辆脸,并且采用過(guò)于精細(xì)的比較去判斷。
文中舉得一個(gè)例子是測(cè)試一段Log代碼輸出螃诅,簡(jiǎn)單的判斷l(xiāng)og要和預(yù)定的一套String完全一樣啡氢。這樣的問(wèn)題在于如果稍微改一下log的格式,比如時(shí)間顯示格式(假設(shè)測(cè)試的不是時(shí)間格式對(duì)不對(duì))州刽,測(cè)試就會(huì)通不過(guò),而且不看輸出細(xì)節(jié)還不知道到底是哪里輸出錯(cuò)了(理想情況是希望通過(guò)方法名直接知道什么錯(cuò)誤而不需要看output)浪箭。
不過(guò)我個(gè)人覺(jué)得穗椅,這種超斷言在有些特殊情況下也是可以用的,比如一些UI測(cè)試直接用了截屏比較圖片的方式奶栖,可以認(rèn)為是很脆弱的超斷言匹表,因?yàn)楹?jiǎn)單的CSS改動(dòng)就會(huì)導(dǎo)致測(cè)試通不過(guò),但如果每個(gè)UI對(duì)象去測(cè)試的維護(hù)成本更高或者根本難以寫出這種測(cè)試的話宣鄙,圖片比較方式也是可以接受的甚至是唯一現(xiàn)實(shí)可行的方案袍镀。
這在我們系統(tǒng)中有個(gè)例子,就是Order Interface的測(cè)試冻晤,為了簡(jiǎn)單起見苇羡,直接拿接口的xml輸出去和一個(gè)認(rèn)為正確的文本比較是否完全一致,雖然寫起來(lái)簡(jiǎn)單鼻弧,也導(dǎo)致了測(cè)試的脆弱性设江,其中利弊需要權(quán)衡。逐比特位斷言 (Bitwise assertions)
這個(gè)其實(shí)也不用單獨(dú)寫出來(lái)攘轩,是一種特殊的原始斷言叉存,給個(gè)例子看看就明白了。
public class PlatformTest {
@Test
public void platformBitLength() {
assertTrue(Platform.IS_32_BIT ^ Platform.IS_64_BIT);
}
}
應(yīng)該改成下面這樣
public class PlatformTest {
@Test
public void platformBitLength() {
assertTrue("Not 32 or 64-bit platform?",
Platform.IS_32_BIT || Platform.IS_64_BIT);
assertFalse("Can’t be 32 and 64-bit at the same time.",
Platform.IS_32_BIT && Platform.IS_64_BIT);
}
}
附加的細(xì)節(jié)(Incidental details)
這個(gè)是說(shuō)單段的測(cè)試代碼太長(zhǎng)了度帮,所有東西寫在一個(gè)方法里歼捏,抽象層次混雜,無(wú)關(guān)的細(xì)節(jié)太多, 讓人一眼看不出代碼到底想要干啥瞳秽,從而可讀性不好瓣履。解決辦法是抽方法出來(lái),讓測(cè)試方法保持在同一個(gè)抽象級(jí)別上寂诱。具體就不說(shuō)了拂苹。分裂的人格 (Split personality)
簡(jiǎn)單說(shuō)來(lái)就是同一個(gè)測(cè)試方法里斷言了幾個(gè)不太相關(guān)的東西,可以說(shuō)是不同的人格(personality)或者不同的興趣點(diǎn)(interest)痰洒。當(dāng)然瓢棒,如何劃分所謂不想關(guān)的事情需要具體情況具體分析,也可能不同人理解不同丘喻。根據(jù)不同情況脯宿,我們采取的措施可以是簡(jiǎn)單拆成不同的測(cè)試方法。如果拆方法還不夠泉粉,那我們自然還可以拆成不同的測(cè)試類(相同部分可以提取抽象測(cè)試基類)连霉。分裂的邏輯 (Split logic)
這說(shuō)的是測(cè)試的代碼過(guò)于長(zhǎng)導(dǎo)致看了后面的忘了前面的,或者邏輯被分隔在不同的文件中嗡靡,看了這個(gè)文件忘了那個(gè)文件跺撼。
書中的例子如下
public class TestRuby {
private Ruby runtime;
@Before
public void setUp() throws Exception {
runtime = Ruby.newInstance();
}
@Test
public void testVarAndMet() throws Exception {
runtime.getLoadService().init(new ArrayList());
eval("load 'test/testVariableAndMethod.rb'");
assertEquals("Hello World", eval("puts($a)"));
assertEquals("dlroW olleH", eval("puts $b"));
assertEquals("Hello World",
eval("puts $d.reverse, $c, $e.reverse"));
assertEquals("135 20 3",
eval("puts $f, \" \", $g, \" \", $h"));
}
}
上面這段代碼的最大問(wèn)題就在于 eval("load 'test/testVariableAndMethod.rb'"); 這句話。這個(gè)測(cè)試從外部加載了另一個(gè)ruby文件讨彼,讀者不兩個(gè)文件對(duì)比著看歉井,根本不知道在測(cè)試什么。
解決辦法是哈误,如果testVariableAndMethod.rb這個(gè)文件內(nèi)容不多的話哩至,直接把文件內(nèi)容插在測(cè)試代碼里(inline)。如果一定要分開放的話蜜自,要和測(cè)試放在同一個(gè)目錄下面菩貌,要能通過(guò)相對(duì)路徑來(lái)訪問(wèn)。
- 魔法數(shù)(Magic number)
這個(gè)很好理解重荠,一般來(lái)說(shuō)解決方法是抽取常量箭阶。不過(guò)書中還寫了一些特殊方式來(lái)解決,比如用獨(dú)特的方法名來(lái)表明入?yún)⒌暮x戈鲁。見下面這個(gè)例子
public class BowlingGameTest {
@Test
public void perfectGame() throws Exception {
roll(pins(10), times(12));
assertThat(game.score(), is(equalTo(300)));
}
private int pins(int n) { return n; }
private int times(int n) { return n; }
}
過(guò)長(zhǎng)的初始化 (Setup sermon)
就是說(shuō)寫了一大堆的代碼用于初始化測(cè)試所需的fixture尾膊,是一種特殊情況的Incidental details。解決方法無(wú)非抽方法荞彼,變量取好名字冈敛,保持同一方法中的抽象層級(jí)一致性。過(guò)保護(hù)的測(cè)試(Overprotective tests)
測(cè)試一些非核心的并不是你測(cè)試代碼需要測(cè)試的內(nèi)容鸣皂。
@Test
public void count() {
Data data = project.getData();
assertNotNull(data);
assertEquals(4, data.count());
}
上面代碼中的第一個(gè)null斷言不需要寫抓谴,因?yàn)檫@并不是這個(gè)測(cè)試方法所要測(cè)試的邏輯暮蹂。雖然說(shuō)隱式的假設(shè)data不為空不是完全正確,但是顯式去判斷這種無(wú)關(guān)緊要的東西會(huì)影響可讀性癌压。另外就算不寫這句話仰泻,如果data為null了測(cè)試同樣會(huì)拋錯(cuò)導(dǎo)致測(cè)試失敗,所以也不會(huì)影響測(cè)試結(jié)果滩届。
可讀性的code smell到此介紹結(jié)束集侯,下一章將介紹可維護(hù)性。