文中圖片來源互聯(lián)網(wǎng)
“Unit testing is often talked about in software development, and is a term that I've been familiar with during my whole time writing programs. Like most software development terminology, however, it's very ill-defined, and I see confusion can often occur when people think that it's more tightly defined than it actually is. — Martin Fowler”
因何而生
正如Martin Fowler說的,單元測試這個(gè)詞在軟件開發(fā)中高頻次地出現(xiàn),它聚焦于系統(tǒng)的某一部分,通常是較低級別,比如類、方法等,是一種以較小代價(jià)換取軟件“正確”的方法。這里提到了一個(gè)很重要的概念,即怎么樣理解軟件“正確”?
在我看來軟件的正確性包含以下幾個(gè)點(diǎn):
- 流程符合預(yù)期,即按照設(shè)計(jì)的步驟運(yùn)行,并在關(guān)鍵的步驟執(zhí)行正確的功能,包含正確的參數(shù);
- 執(zhí)行效果符合預(yù)期,即通過功能執(zhí)行后,能夠產(chǎn)生符合設(shè)計(jì)的結(jié)果,這種結(jié)果可以是直接的,比如方法的返回值;也可以是間接的,比如方法改變了實(shí)例中可被觀察到的部分;
- 異常保護(hù)符合預(yù)期,功能執(zhí)行過程會(huì)遭遇超越設(shè)計(jì)邊界的場景,保護(hù)自身不因“越界”而失效;
- 質(zhì)量屬性符合預(yù)期,某些功能具有質(zhì)量屬性,如響應(yīng)時(shí)間等。
單元測試應(yīng)圍繞上述理解展開,它需要且應(yīng)當(dāng)說明被測功能的正確,比如下面這些我們常見的單元測試寫法。
public class AppTest {
// 驗(yàn)證流程符合預(yù)期的測試
@Test
public void should_give_tips_when_input_length_not_4() throws Exception {
// given
// when
when(interactable.read()).thenReturn("231").thenReturn("1234");
app.play();
// then
verify(interactable).write("Please input 4 none repeatable numbers(You have 6 times).");
}
// 驗(yàn)證執(zhí)行效果符合預(yù)期的測試
@Test
public void should_out_buzz_when_number_is_multiples_of_five() throws Exception {
// given
// when
final String result = rule.transform(5);
// then
assertThat(result, is("Buzz"));
}
// 驗(yàn)證異常保護(hù)符合預(yù)期的測試
@Test(expected = Exception.class)
public should_throw_exception_when_input_invalid() throws Exception {
// given
// when
// then
}
}
覆蓋率或測試覆蓋率是用來衡量單元測試對功能代碼的測試情況,通過統(tǒng)計(jì)單元測試中對功能代碼中行、分支、類等模擬場景數(shù)量,來量化說明測試的充分度。覆蓋率的前提是存在單元測試,并且從其本意上推導(dǎo),可被統(tǒng)計(jì)覆蓋率的單元測試應(yīng)當(dāng)是證明了軟件正確的,這是一個(gè)不能動(dòng)搖的基礎(chǔ),否則一切就失去意義。

從上述分析不難看出單元測試與覆蓋率的側(cè)重點(diǎn)是不一樣的,單元測試重點(diǎn)在于驗(yàn)證軟件正確,而覆蓋率重點(diǎn)在于描述測試的充分程度,兩者不會(huì)等同起來,但在項(xiàng)目和團(tuán)隊(duì)中一個(gè)普遍的認(rèn)識是“高覆蓋率的代碼,其功能的正確性是得到保障的”。
誤入歧途
覆蓋率在持續(xù)集成中一般會(huì)作為代碼準(zhǔn)入的標(biāo)準(zhǔn),這種選擇來源于原則“沒有測試覆蓋的代碼是不可靠的”以及它的變化衍生。大多數(shù)項(xiàng)目都會(huì)設(shè)定一個(gè)覆蓋率的門限值,禁止無測試的代碼合入同時(shí)還要警告覆蓋率的降低。通常來說這么做是合理的,持續(xù)集成中覆蓋率檢查以一種顯性的約束來規(guī)范開發(fā)人員使用單元測試保障開發(fā)代碼的正確性,并讓單元測試逐漸地變成開發(fā)習(xí)慣。不得不說,覆蓋率檢查對單元測試的普及起了十分積極的作用。
但最近的一些發(fā)現(xiàn)讓我對覆蓋率的認(rèn)識產(chǎn)生了一些擔(dān)憂,在走查代碼的過程中發(fā)現(xiàn)了一些寫法十分奇特的單元測試,類似下面代碼:
public class GameTest {
@Test
public void testVerify() throws Exception {
// given
// when
new Game("1234").verify("1234");
// then
}
}
這樣的代碼乍看是沒有什么問題的,使用了測試框架,調(diào)用了被測對象的外部接口,覆蓋率報(bào)告上也有體現(xiàn),一句話——完美!

但細(xì)探一下就發(fā)現(xiàn)如此完美的測試代碼偏偏少了最重要的東西——對預(yù)期的判斷,就是我們上面提到的軟件正確性的4點(diǎn)。這就太糟糕了,因?yàn)槌善臏y試根本沒有辦法告訴開發(fā)人員他們寫的代碼究竟是否正確,既然沒有了對錯(cuò)那么單元測試的意義又何在呢?

為何會(huì)出現(xiàn)上面的測試呢?在與開發(fā)人員交流后發(fā)現(xiàn)覆蓋率在這其中起了很大的因素。當(dāng)項(xiàng)目劃定了代碼準(zhǔn)入的覆蓋率門限后,在短時(shí)間內(nèi)大量的代碼是無法提交入庫的,而項(xiàng)目又對功能發(fā)布有較強(qiáng)的deadline,在這兩種因素的共同作用下,就會(huì)有人想到上述的“奇招”,這樣的測試不會(huì)檢驗(yàn)功能的正確,而會(huì)產(chǎn)生符合要求的高覆蓋率。更糟糕的是,由于無法驗(yàn)證功能的正確即無法產(chǎn)生價(jià)值于開發(fā)人員,那么測試這件事就會(huì)受到抵制,同時(shí)測試代碼也會(huì)耗散有限的迭代時(shí)間,造成對單元測試的認(rèn)同更加低落,使得一些本可以逐漸落地的方法,如測試驅(qū)動(dòng)開發(fā),變成空中樓閣。
正本清源
現(xiàn)在再回頭看我們之前提到的關(guān)于單元測試和覆蓋率的普遍認(rèn)識:“高覆蓋率的代碼,其功能的正確性是得到保障的”,你還認(rèn)為這句話一定正確嗎?
單元測試的目的是為了以較小的代價(jià)(白盒)換取軟件正確,而覆蓋率的目的是在有效單元測試的基礎(chǔ)上統(tǒng)計(jì)測試代碼測試被測對象的充分程度。兩者存在聯(lián)系卻不能相互替換。誠然,在保證單元測試實(shí)現(xiàn)其目的的情況下,上述認(rèn)識才真正變得有意義,如果混淆了單元測試和覆蓋率的意義,那么就會(huì)出現(xiàn)上面的舍本逐末,此時(shí)寫再多的測試也不能證明軟件的正確,只能證明你對單元測試和覆蓋率的誤解有多深!