邁出單元測(cè)試的第一步
單元測(cè)試不僅是軟件行業(yè)的最佳實(shí)踐,在敏捷方法的推動(dòng)下,它也成為了可持續(xù)軟件生產(chǎn)的支柱。根據(jù)最新的年度敏捷調(diào)查,70%的參與者會(huì)對(duì)他們的代碼進(jìn)行單元測(cè)試。
單元測(cè)試和其他敏捷實(shí)踐密切相關(guān),所以開始編寫測(cè)試是組織向敏捷轉(zhuǎn)型的踏腳石。道路漫長(zhǎng),但值得去做。我將在本文介紹符合要求的小技巧,以及在開發(fā)周期里進(jìn)行單元測(cè)試的步驟。
有效的單元測(cè)試默認(rèn)要能自動(dòng)化。沒(méi)有自動(dòng)化,生產(chǎn)力就會(huì)下降。沒(méi)有自動(dòng)化,單元測(cè)試的習(xí)慣也不會(huì)持續(xù)太久。依靠手工測(cè)試(由測(cè)試人員或開發(fā)人員完成)并不能持續(xù)太長(zhǎng)時(shí)間;在有壓力的情況下,沒(méi)人會(huì)記得去運(yùn)行所有的測(cè)試,或者去覆蓋所有的場(chǎng)景。自動(dòng)化是我們的朋友,所有的單元測(cè)試框架都支持自動(dòng)化,而且集成了其他自動(dòng)化系統(tǒng)。
單元測(cè)試對(duì)現(xiàn)代開發(fā)來(lái)說(shuō)至關(guān)重要
有代碼相關(guān)的測(cè)試,我們就有一個(gè)天然的安全保障。我們修改的代碼要是帶來(lái)了什么問(wèn)題,測(cè)試會(huì)告訴我們。這個(gè)安全保障越健全,我們對(duì)代碼正常運(yùn)行的信心就越大,對(duì)按需修改代碼的能力也就越有信心。
和其他類型的測(cè)試相比,單元測(cè)試的主要優(yōu)點(diǎn)是反饋迅速。在幾秒鐘內(nèi)運(yùn)行數(shù)百個(gè)成套的測(cè)試,這對(duì)開發(fā)流程很有幫助。我們會(huì)形成“添加一些代碼,添加測(cè)試,測(cè)試運(yùn)行通過(guò),前進(jìn)”的節(jié)奏。小步前進(jìn)、確保一切正常也意味著調(diào)試時(shí)間會(huì)大大減少。測(cè)試能提高生產(chǎn)力也就不足為奇了——在Bug上少花時(shí)間,把更多的時(shí)間用到新功能的推出上。
依賴關(guān)系的壁壘
給新建項(xiàng)目添加測(cè)試相當(dāng)容易——畢竟代碼不會(huì)阻礙測(cè)試。不過(guò)這種情況絕對(duì)不常見(jiàn)。大多數(shù)人都是在處理遺留代碼,這些代碼不太容易測(cè)試,有時(shí)候甚至運(yùn)行不起來(lái)——它需要的數(shù)據(jù)或配置可能只存在于生產(chǎn)服務(wù)器上。我們或許要為不同的場(chǎng)景創(chuàng)建不同的設(shè)置,這也許會(huì)花費(fèi)過(guò)多的精力。在很多情況下,我們可能還會(huì)為了測(cè)試修改代碼。這讓人無(wú)法理解:我們編寫測(cè)試就是為了能有修改代碼的信心,還沒(méi)有測(cè)試又該如何去穩(wěn)妥地修改代碼呢?
代碼可測(cè)性是語(yǔ)言和工具的功能。大家認(rèn)為Ruby等動(dòng)態(tài)語(yǔ)言是可測(cè)的。對(duì)于測(cè)試的內(nèi)部代碼,我們可以改變其依賴關(guān)系的行為,而不用修改生產(chǎn)代碼。C#或Java等靜態(tài)類型語(yǔ)言則不太容易去測(cè)試。
下面有個(gè)例子:一個(gè)C#的過(guò)期檢查方法,檢查是否超過(guò)了特定日期:
public class ExpirationChecker
{
private readonly DateTime expirationDate = new DateTime(2012, 1, 1);
public bool IsExpired()
{
if (DateTime.Now > expirationDate)
{
return true;
}
return false;
}
}
在這個(gè)例子里,IsExpired方法的DateTime屬性對(duì)測(cè)試運(yùn)行時(shí)間有強(qiáng)依賴。Now返回的是實(shí)際時(shí)間。這個(gè)方法有兩種情況,它會(huì)根據(jù)日期返回不同的值。修改計(jì)算機(jī)時(shí)間是絕對(duì)不行的,因?yàn)槲覀円谌魏螘r(shí)候到任何計(jì)算機(jī)上去測(cè)試場(chǎng)景,并且不能帶來(lái)任何副作用。
要測(cè)試到兩種情況,一種可能的解決方案是修改代碼。比如說(shuō),我們可以把代碼修改成:
public bool IsExpired(DateTime now)
{
if (now > expirationDate)
{
return true;
}
return false;
}
這樣,測(cè)試可以注入不同、可控的DateTime值,而不用在生產(chǎn)代碼里寫定一個(gè)值。我們要是不能修改代碼,可以利用Typemock Isolator等Mocking框架,模擬靜態(tài)屬性和方法。針對(duì)先前的代碼,測(cè)試可以寫成:
[TestMethod]
public void IsExpired_BeforeExpirationDate_ReturnFalse()
{
Isolate.WhenCalled(() => DateTime.Now)
.WillReturn(new DateTime(2000, 1, 1));
ExpirationChecker checker = new ExpirationChecker();
var result = checker.IsExpired();
Assert.IsFalse(result);
}
現(xiàn)有的遺留代碼不能輕易修改,因?yàn)槲覀儧](méi)有針對(duì)它的測(cè)試。開始測(cè)試遺留代碼之后,我們就能明白:代碼越丑陋,測(cè)試越困難。工具可以減輕一些痛苦,但我們要努力去構(gòu)建安全的環(huán)境。
依賴關(guān)系并不是唯一的內(nèi)容……
我們很快會(huì)遇到的另一個(gè)問(wèn)題是測(cè)試維護(hù):測(cè)試和被測(cè)試代碼耦合在一起。有耦合關(guān)系,修改生產(chǎn)代碼就有可能破壞測(cè)試。要是代碼修改引起測(cè)試失敗,我們就需要回去解決這些問(wèn)題。很多開發(fā)人員害怕維護(hù)兩個(gè)代碼庫(kù),這種恐懼甚至?xí)屗麄兏纱嗖贿M(jìn)行單元測(cè)試。真正的維護(hù)工作既取決于工具,也取決于技巧。
編寫好的測(cè)試是通過(guò)實(shí)踐獲得的技能。編寫的測(cè)試越多,我們就越精于此,同時(shí)會(huì)提升測(cè)試質(zhì)量,維護(hù)也越來(lái)越少。有了測(cè)試,我們就有機(jī)會(huì)重構(gòu)代碼,這反過(guò)來(lái)又會(huì)讓測(cè)試更簡(jiǎn)潔、更易讀、更健壯。
工具對(duì)實(shí)踐的難易程度有極大的影響。在基礎(chǔ)層,我們需要一個(gè)測(cè)試框架和一個(gè)Mocking框架。在.Net領(lǐng)域,兩種框架的選擇都很豐富。
編寫第一個(gè)測(cè)試的準(zhǔn)則
開始的時(shí)候,我們通常會(huì)試用不同的工具,來(lái)理解他們的工作原理。我們往往不會(huì)在實(shí)際的工作代碼上開始編寫測(cè)試。但很快就要給代碼編寫真正的測(cè)試。有一些小提示屆時(shí)會(huì)有用:
● 從哪里開始:一般來(lái)說(shuō),我們編寫測(cè)試是針對(duì)工作代碼的,無(wú)論代碼是Bug修復(fù)還是新功能。對(duì)Bug修復(fù)來(lái)說(shuō),編寫的測(cè)試要檢查修復(fù)。對(duì)功能來(lái)說(shuō),測(cè)試應(yīng)檢查正確的行為。
● 支架:以我們掌握的知識(shí)來(lái)看,明智的做法是先添加能確保當(dāng)前實(shí)現(xiàn)運(yùn)行的測(cè)試。添加新的代碼之前先寫測(cè)試,因?yàn)槲覀兿M谛薷默F(xiàn)有代碼之前,能有安全的保障。這些測(cè)試被稱為“特征測(cè)試”,這個(gè)術(shù)語(yǔ)來(lái)自Michael Feathers編寫的《修改代碼的藝術(shù)》。
● 命名:測(cè)試最重要的屬性是它的名字。我們一般不會(huì)去看運(yùn)行通過(guò)的測(cè)試。但當(dāng)它失敗時(shí),我們看的就是它的名字。所以挑一個(gè)好名字,描述出場(chǎng)景和代碼的預(yù)期結(jié)果。好名字還有助于我們定位測(cè)試?yán)锏腂ug。
● 評(píng)審:為了增加測(cè)試成功通過(guò)的機(jī)會(huì),編寫第一個(gè)測(cè)試時(shí)我們應(yīng)該和同事結(jié)對(duì)。兩個(gè)人都能從實(shí)踐中學(xué)習(xí),而且我們還能立即評(píng)審測(cè)試。最好對(duì)所測(cè)的內(nèi)容、測(cè)試的名稱達(dá)成共識(shí),因?yàn)檫@會(huì)成為團(tuán)隊(duì)其他人員的基本模板。
● AAA:現(xiàn)代測(cè)試的結(jié)構(gòu)符合AAA模式——Arrange(測(cè)試設(shè)置)、Act(調(diào)用測(cè)試?yán)锏拇a)、Assert(測(cè)試通過(guò)的標(biāo)準(zhǔn))。如果我們使用測(cè)試驅(qū)動(dòng)開發(fā)(TDD),我們要先編寫完整的測(cè)試,然后再添加代碼。對(duì)遺留代碼來(lái)說(shuō),我們可能需要換一種方式。一旦我們有一個(gè)場(chǎng)景和名稱需要測(cè)試,那先編寫Act和Assert部分。我們要不停構(gòu)建Arrange部分,因?yàn)閷?duì)需要準(zhǔn)備或仿造的依賴關(guān)系,我們知道的要更多一些。然后繼續(xù)這么做,直到有一個(gè)測(cè)試能夠通過(guò)。
● 重構(gòu):一旦準(zhǔn)備好了測(cè)試,我們就可以重構(gòu)代碼了。重構(gòu)和測(cè)試都是后天獲得的技能。我們不僅要重構(gòu)被測(cè)試代碼,也要重構(gòu)測(cè)試本身。但DRY(不要重復(fù)自己)原則不適用于測(cè)試。測(cè)試失敗時(shí),我們希望盡快修復(fù)問(wèn)題,所有的測(cè)試代碼最好在一個(gè)地方,而不是分散在不同的文件里。
● 可讀性:測(cè)試應(yīng)該是可讀的,最好是人類可讀。和搭檔評(píng)審測(cè)試代碼,看他能否理解測(cè)試的目的。評(píng)審其他測(cè)試,看看它們的名稱和內(nèi)容怎樣與相鄰的測(cè)試區(qū)分開來(lái)。一旦測(cè)試失敗,就需要修復(fù)它們,最好還是在運(yùn)行失敗之前評(píng)審它們。
● 組織:一旦我們有了更多的測(cè)試,組織就有了用武之地。測(cè)試可以在很多方面有所不同,但最明顯的一個(gè)就是如何快速運(yùn)行。有些測(cè)試可能在毫秒內(nèi)運(yùn)行完,而有些則需要數(shù)秒或好幾分鐘。和工作一樣,我們都希望得到最快的反饋。這就是前面談到的怎么按一定的節(jié)奏去進(jìn)行。要做到這一點(diǎn),你應(yīng)該把測(cè)試劃分一下,把快的測(cè)試和慢的測(cè)試分開運(yùn)行。這能手工(努力)去做,但在.NET領(lǐng)域,Typemock Isolator有一個(gè)運(yùn)行器,能自動(dòng)按運(yùn)行速度分離。
總結(jié)
邁出單元測(cè)試的第一步是很有挑戰(zhàn)的。體驗(yàn)依賴的東西很多——語(yǔ)言、工具、現(xiàn)有代碼、依賴關(guān)系和技能。只要稍稍思考,進(jìn)行大量訓(xùn)練和實(shí)踐,你就能漸入測(cè)試的佳境。
單元測(cè)試不僅是軟件行業(yè)的最佳實(shí)踐,在敏捷方法的推動(dòng)下,它也成為了可持續(xù)軟件生產(chǎn)的支柱。根據(jù)最新的年度敏捷調(diào)查,70%的參與者會(huì)對(duì)他們的代碼進(jìn)行單元測(cè)試。
單元測(cè)試和其他敏捷實(shí)踐密切相關(guān),所以開始編寫測(cè)試是組織向敏捷轉(zhuǎn)型的踏腳石。道路漫長(zhǎng),但值得去做。我將在本文介紹符合要求的小技巧,以及在開發(fā)周期里進(jìn)行單元測(cè)試的步驟。
有效的單元測(cè)試默認(rèn)要能自動(dòng)化。沒(méi)有自動(dòng)化,生產(chǎn)力就會(huì)下降。沒(méi)有自動(dòng)化,單元測(cè)試的習(xí)慣也不會(huì)持續(xù)太久。依靠手工測(cè)試(由測(cè)試人員或開發(fā)人員完成)并不能持續(xù)太長(zhǎng)時(shí)間;在有壓力的情況下,沒(méi)人會(huì)記得去運(yùn)行所有的測(cè)試,或者去覆蓋所有的場(chǎng)景。自動(dòng)化是我們的朋友,所有的單元測(cè)試框架都支持自動(dòng)化,而且集成了其他自動(dòng)化系統(tǒng)。
單元測(cè)試對(duì)現(xiàn)代開發(fā)來(lái)說(shuō)至關(guān)重要
有代碼相關(guān)的測(cè)試,我們就有一個(gè)天然的安全保障。我們修改的代碼要是帶來(lái)了什么問(wèn)題,測(cè)試會(huì)告訴我們。這個(gè)安全保障越健全,我們對(duì)代碼正常運(yùn)行的信心就越大,對(duì)按需修改代碼的能力也就越有信心。
和其他類型的測(cè)試相比,單元測(cè)試的主要優(yōu)點(diǎn)是反饋迅速。在幾秒鐘內(nèi)運(yùn)行數(shù)百個(gè)成套的測(cè)試,這對(duì)開發(fā)流程很有幫助。我們會(huì)形成“添加一些代碼,添加測(cè)試,測(cè)試運(yùn)行通過(guò),前進(jìn)”的節(jié)奏。小步前進(jìn)、確保一切正常也意味著調(diào)試時(shí)間會(huì)大大減少。測(cè)試能提高生產(chǎn)力也就不足為奇了——在Bug上少花時(shí)間,把更多的時(shí)間用到新功能的推出上。
依賴關(guān)系的壁壘
給新建項(xiàng)目添加測(cè)試相當(dāng)容易——畢竟代碼不會(huì)阻礙測(cè)試。不過(guò)這種情況絕對(duì)不常見(jiàn)。大多數(shù)人都是在處理遺留代碼,這些代碼不太容易測(cè)試,有時(shí)候甚至運(yùn)行不起來(lái)——它需要的數(shù)據(jù)或配置可能只存在于生產(chǎn)服務(wù)器上。我們或許要為不同的場(chǎng)景創(chuàng)建不同的設(shè)置,這也許會(huì)花費(fèi)過(guò)多的精力。在很多情況下,我們可能還會(huì)為了測(cè)試修改代碼。這讓人無(wú)法理解:我們編寫測(cè)試就是為了能有修改代碼的信心,還沒(méi)有測(cè)試又該如何去穩(wěn)妥地修改代碼呢?
代碼可測(cè)性是語(yǔ)言和工具的功能。大家認(rèn)為Ruby等動(dòng)態(tài)語(yǔ)言是可測(cè)的。對(duì)于測(cè)試的內(nèi)部代碼,我們可以改變其依賴關(guān)系的行為,而不用修改生產(chǎn)代碼。C#或Java等靜態(tài)類型語(yǔ)言則不太容易去測(cè)試。
下面有個(gè)例子:一個(gè)C#的過(guò)期檢查方法,檢查是否超過(guò)了特定日期:
public class ExpirationChecker public bool IsExpired() if (DateTime.Now > expirationDate) |
在這個(gè)例子里,IsExpired方法的DateTime屬性對(duì)測(cè)試運(yùn)行時(shí)間有強(qiáng)依賴。Now返回的是實(shí)際時(shí)間。這個(gè)方法有兩種情況,它會(huì)根據(jù)日期返回不同的值。修改計(jì)算機(jī)時(shí)間是絕對(duì)不行的,因?yàn)槲覀円谌魏螘r(shí)候到任何計(jì)算機(jī)上去測(cè)試場(chǎng)景,并且不能帶來(lái)任何副作用。
要測(cè)試到兩種情況,一種可能的解決方案是修改代碼。比如說(shuō),我們可以把代碼修改成:
public bool IsExpired(DateTime now) { if (now > expirationDate) { return true; } return false; } |
這樣,測(cè)試可以注入不同、可控的DateTime值,而不用在生產(chǎn)代碼里寫定一個(gè)值。我們要是不能修改代碼,可以利用Typemock Isolator等Mocking框架,模擬靜態(tài)屬性和方法。針對(duì)先前的代碼,測(cè)試可以寫成:
[TestMethod] ExpirationChecker checker = new ExpirationChecker(); Assert.IsFalse(result); |
現(xiàn)有的遺留代碼不能輕易修改,因?yàn)槲覀儧](méi)有針對(duì)它的測(cè)試。開始測(cè)試遺留代碼之后,我們就能明白:代碼越丑陋,測(cè)試越困難。工具可以減輕一些痛苦,但我們要努力去構(gòu)建安全的環(huán)境。
依賴關(guān)系并不是唯一的內(nèi)容……
我們很快會(huì)遇到的另一個(gè)問(wèn)題是測(cè)試維護(hù):測(cè)試和被測(cè)試代碼耦合在一起。有耦合關(guān)系,修改生產(chǎn)代碼就有可能破壞測(cè)試。要是代碼修改引起測(cè)試失敗,我們就需要回去解決這些問(wèn)題。很多開發(fā)人員害怕維護(hù)兩個(gè)代碼庫(kù),這種恐懼甚至?xí)屗麄兏纱嗖贿M(jìn)行單元測(cè)試。真正的維護(hù)工作既取決于工具,也取決于技巧。
編寫好的測(cè)試是通過(guò)實(shí)踐獲得的技能。編寫的測(cè)試越多,我們就越精于此,同時(shí)會(huì)提升測(cè)試質(zhì)量,維護(hù)也越來(lái)越少。有了測(cè)試,我們就有機(jī)會(huì)重構(gòu)代碼,這反過(guò)來(lái)又會(huì)讓測(cè)試更簡(jiǎn)潔、更易讀、更健壯。
工具對(duì)實(shí)踐的難易程度有極大的影響。在基礎(chǔ)層,我們需要一個(gè)測(cè)試框架和一個(gè)Mocking框架。在.Net領(lǐng)域,兩種框架的選擇都很豐富。
編寫第一個(gè)測(cè)試的準(zhǔn)則
開始的時(shí)候,我們通常會(huì)試用不同的工具,來(lái)理解他們的工作原理。我們往往不會(huì)在實(shí)際的工作代碼上開始編寫測(cè)試。但很快就要給代碼編寫真正的測(cè)試。有一些小提示屆時(shí)會(huì)有用:
● 從哪里開始:一般來(lái)說(shuō),我們編寫測(cè)試是針對(duì)工作代碼的,無(wú)論代碼是Bug修復(fù)還是新功能。對(duì)Bug修復(fù)來(lái)說(shuō),編寫的測(cè)試要檢查修復(fù)。對(duì)功能來(lái)說(shuō),測(cè)試應(yīng)檢查正確的行為。
● 支架:以我們掌握的知識(shí)來(lái)看,明智的做法是先添加能確保當(dāng)前實(shí)現(xiàn)運(yùn)行的測(cè)試。添加新的代碼之前先寫測(cè)試,因?yàn)槲覀兿M谛薷默F(xiàn)有代碼之前,能有安全的保障。這些測(cè)試被稱為“特征測(cè)試”,這個(gè)術(shù)語(yǔ)來(lái)自Michael Feathers編寫的《修改代碼的藝術(shù)》。
● 命名:測(cè)試最重要的屬性是它的名字。我們一般不會(huì)去看運(yùn)行通過(guò)的測(cè)試。但當(dāng)它失敗時(shí),我們看的就是它的名字。所以挑一個(gè)好名字,描述出場(chǎng)景和代碼的預(yù)期結(jié)果。好名字還有助于我們定位測(cè)試?yán)锏腂ug。
● 評(píng)審:為了增加測(cè)試成功通過(guò)的機(jī)會(huì),編寫第一個(gè)測(cè)試時(shí)我們應(yīng)該和同事結(jié)對(duì)。兩個(gè)人都能從實(shí)踐中學(xué)習(xí),而且我們還能立即評(píng)審測(cè)試。最好對(duì)所測(cè)的內(nèi)容、測(cè)試的名稱達(dá)成共識(shí),因?yàn)檫@會(huì)成為團(tuán)隊(duì)其他人員的基本模板。
● AAA:現(xiàn)代測(cè)試的結(jié)構(gòu)符合AAA模式——Arrange(測(cè)試設(shè)置)、Act(調(diào)用測(cè)試?yán)锏拇a)、Assert(測(cè)試通過(guò)的標(biāo)準(zhǔn))。如果我們使用測(cè)試驅(qū)動(dòng)開發(fā)(TDD),我們要先編寫完整的測(cè)試,然后再添加代碼。對(duì)遺留代碼來(lái)說(shuō),我們可能需要換一種方式。一旦我們有一個(gè)場(chǎng)景和名稱需要測(cè)試,那先編寫Act和Assert部分。我們要不停構(gòu)建Arrange部分,因?yàn)閷?duì)需要準(zhǔn)備或仿造的依賴關(guān)系,我們知道的要更多一些。然后繼續(xù)這么做,直到有一個(gè)測(cè)試能夠通過(guò)。
● 重構(gòu):一旦準(zhǔn)備好了測(cè)試,我們就可以重構(gòu)代碼了。重構(gòu)和測(cè)試都是后天獲得的技能。我們不僅要重構(gòu)被測(cè)試代碼,也要重構(gòu)測(cè)試本身。但DRY(不要重復(fù)自己)原則不適用于測(cè)試。測(cè)試失敗時(shí),我們希望盡快修復(fù)問(wèn)題,所有的測(cè)試代碼最好在一個(gè)地方,而不是分散在不同的文件里。
● 可讀性:測(cè)試應(yīng)該是可讀的,最好是人類可讀。和搭檔評(píng)審測(cè)試代碼,看他能否理解測(cè)試的目的。評(píng)審其他測(cè)試,看看它們的名稱和內(nèi)容怎樣與相鄰的測(cè)試區(qū)分開來(lái)。一旦測(cè)試失敗,就需要修復(fù)它們,最好還是在運(yùn)行失敗之前評(píng)審它們。
● 組織:一旦我們有了更多的測(cè)試,組織就有了用武之地。測(cè)試可以在很多方面有所不同,但最明顯的一個(gè)就是如何快速運(yùn)行。有些測(cè)試可能在毫秒內(nèi)運(yùn)行完,而有些則需要數(shù)秒或好幾分鐘。和工作一樣,我們都希望得到最快的反饋。這就是前面談到的怎么按一定的節(jié)奏去進(jìn)行。要做到這一點(diǎn),你應(yīng)該把測(cè)試劃分一下,把快的測(cè)試和慢的測(cè)試分開運(yùn)行。這能手工(努力)去做,但在.NET領(lǐng)域,Typemock Isolator有一個(gè)運(yùn)行器,能自動(dòng)按運(yùn)行速度分離。
總結(jié)
邁出單元測(cè)試的第一步是很有挑戰(zhàn)的。體驗(yàn)依賴的東西很多——語(yǔ)言、工具、現(xiàn)有代碼、依賴關(guān)系和技能。只要稍稍思考,進(jìn)行大量訓(xùn)練和實(shí)踐,你就能漸入測(cè)試的佳境。
posted on 2012-07-30 09:59 順其自然EVO 閱讀(199) 評(píng)論(0) 編輯 收藏 所屬分類: 測(cè)試學(xué)習(xí)專欄