文中圖片來源互聯(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說的歇拆,單元測試這個詞在軟件開發(fā)中高頻次地出現(xiàn),它聚焦于系統(tǒng)的某一部分,通常是較低級別印蔬,比如類、方法等,是一種以較小代價換取軟件“正確”的方法蹭睡。這里提到了一個很重要的概念,即怎么樣理解軟件“正確”赶么?
在我看來軟件的正確性包含以下幾個點(diǎn):
- 流程符合預(yù)期肩豁,即按照設(shè)計的步驟運(yùn)行,并在關(guān)鍵的步驟執(zhí)行正確的功能辫呻,包含正確的參數(shù)清钥;
- 執(zhí)行效果符合預(yù)期,即通過功能執(zhí)行后放闺,能夠產(chǎn)生符合設(shè)計的結(jié)果祟昭,這種結(jié)果可以是直接的,比如方法的返回值怖侦;也可以是間接的篡悟,比如方法改變了實(shí)例中可被觀察到的部分;
- 異常保護(hù)符合預(yù)期匾寝,功能執(zhí)行過程會遭遇超越設(shè)計邊界的場景搬葬,保護(hù)自身不因“越界”而失效;
- 質(zhì)量屬性符合預(yù)期艳悔,某些功能具有質(zhì)量屬性急凰,如響應(yīng)時間等。
單元測試應(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)計單元測試中對功能代碼中行、分支锭碳、類等模擬場景數(shù)量袁稽,來量化說明測試的充分度。覆蓋率的前提是存在單元測試擒抛,并且從其本意上推導(dǎo)推汽,可被統(tǒng)計覆蓋率的單元測試應(yīng)當(dāng)是證明了軟件正確的补疑,這是一個不能動搖的基礎(chǔ),否則一切就失去意義歹撒。
從上述分析不難看出單元測試與覆蓋率的側(cè)重點(diǎn)是不一樣的莲组,單元測試重點(diǎn)在于驗(yàn)證軟件正確,而覆蓋率重點(diǎn)在于描述測試的充分程度暖夭,兩者不會等同起來锹杈,但在項(xiàng)目和團(tuán)隊(duì)中一個普遍的認(rèn)識是“高覆蓋率的代碼,其功能的正確性是得到保障的”迈着。
誤入歧途
覆蓋率在持續(xù)集成中一般會作為代碼準(zhǔn)入的標(biāo)準(zhǔn)竭望,這種選擇來源于原則“沒有測試覆蓋的代碼是不可靠的”以及它的變化衍生。大多數(shù)項(xiàng)目都會設(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)用了被測對象的外部接口,覆蓋率報告上也有體現(xiàn)愚墓,一句話——完美予权!
但細(xì)探一下就發(fā)現(xiàn)如此完美的測試代碼偏偏少了最重要的東西——對預(yù)期的判斷,就是我們上面提到的軟件正確性的4點(diǎn)浪册。這就太糟糕了扫腺,因?yàn)槌善臏y試根本沒有辦法告訴開發(fā)人員他們寫的代碼究竟是否正確,既然沒有了對錯那么單元測試的意義又何在呢村象?
為何會出現(xiàn)上面的測試呢笆环?在與開發(fā)人員交流后發(fā)現(xiàn)覆蓋率在這其中起了很大的因素。當(dāng)項(xiàng)目劃定了代碼準(zhǔn)入的覆蓋率門限后厚者,在短時間內(nèi)大量的代碼是無法提交入庫的躁劣,而項(xiàng)目又對功能發(fā)布有較強(qiáng)的deadline,在這兩種因素的共同作用下库菲,就會有人想到上述的“奇招”账忘,這樣的測試不會檢驗(yàn)功能的正確,而會產(chǎn)生符合要求的高覆蓋率。更糟糕的是鳖擒,由于無法驗(yàn)證功能的正確即無法產(chǎn)生價值于開發(fā)人員溉浙,那么測試這件事就會受到抵制,同時測試代碼也會耗散有限的迭代時間蒋荚,造成對單元測試的認(rèn)同更加低落戳稽,使得一些本可以逐漸落地的方法,如測試驅(qū)動開發(fā)期升,變成空中樓閣惊奇。
正本清源
現(xiàn)在再回頭看我們之前提到的關(guān)于單元測試和覆蓋率的普遍認(rèn)識:“高覆蓋率的代碼,其功能的正確性是得到保障的”吓妆,你還認(rèn)為這句話一定正確嗎赊时?
單元測試的目的是為了以較小的代價(白盒)換取軟件正確,而覆蓋率的目的是在有效單元測試的基礎(chǔ)上統(tǒng)計測試代碼測試被測對象的充分程度行拢。兩者存在聯(lián)系卻不能相互替換祖秒。誠然,在保證單元測試實(shí)現(xiàn)其目的的情況下舟奠,上述認(rèn)識才真正變得有意義竭缝,如果混淆了單元測試和覆蓋率的意義,那么就會出現(xiàn)上面的舍本逐末沼瘫,此時寫再多的測試也不能證明軟件的正確抬纸,只能證明你對單元測試和覆蓋率的誤解有多深!