單元測試小技巧
編寫可維護(hù)的節(jié)省時(shí)間和精力的單元測試
發(fā)布日期: 2006-04-17 | 更新日期: 2006-04-17
這篇文章描述了:
| ? |
單元測試的信任
|
| ? |
測試正確事件
|
| ? |
創(chuàng)建維護(hù)測試
|
| ? |
創(chuàng)建易讀測試
|
這些天有很多的關(guān)于單元測試的和在不同的場景下為他們的應(yīng)用程序編寫單元測試(起始于, 我們2005年六月的 MSDN?Magazine 中有關(guān)測試你的數(shù)據(jù)層的文章, Know Thy Code: Simplify Data Layer Unit Testing using Enterprise Services)的討論。這些意味著有很多的開發(fā)者自言自語(或者對于他們的團(tuán)隊(duì))到:“哎,我們也需要開始編寫測試了!”因此他們開始編寫單元測試上面的單元測試直到他們達(dá)到了一個(gè)測試自己已經(jīng)成為問題的程度。或許維護(hù)他們是一個(gè)太過困難,花費(fèi)太長時(shí)間,或者他們并沒有足夠的易讀性以便于理解,更或者他們本身存在bugs有一點(diǎn)是能夠使得我們的開發(fā)人員可以下定決心去做的,那就是: 花費(fèi)他們寶貴的時(shí)間以用來改進(jìn)提高他們的測試或者忽略其中的問題, 從而有效的甩掉那些艱苦的工作。而這些困難的原因僅僅是因?yàn)槟切┎皇炀毜膶懭雴卧獪y試。.在這篇文章中,我將為大家?guī)碓谶^去一年多時(shí)間里我在開發(fā),提供咨詢和培訓(xùn)開發(fā)者等方面有總結(jié)出來的一些最重要的練習(xí)和試驗(yàn)。這些小的技巧可以幫助您寫出更有效的,可維護(hù),和魯棒性更好的單元測試。同時(shí)我希望這些總結(jié)和忠告可以幫助您避免一些由于錯(cuò)誤引起的大量的時(shí)間的消耗。
本頁內(nèi)容
單元測試的信任
在這個(gè)部分,我將略述出一些最通用的信任,這些信任來自于在使用大量單元測試獲得的好處和解釋為什么這些信任通常不是必須真實(shí)的。然后我們會幫助您在您的工程中擁有這些信任。
更加簡單的跟蹤
Bug? 當(dāng)然這并不是必須的,那么您怎么知道您的測試是正確的? 是否存在在一些測試環(huán)節(jié)測試失敗的情況?另外您又如何知道您的測試覆蓋了系統(tǒng)中多少的代碼量?是否測試到了程序中的錯(cuò)誤,錯(cuò)誤又在哪里等等的問題。
當(dāng)你在你的單元測試中發(fā)現(xiàn)了bug后又會發(fā)生什么事情哪?你會突然間得到很多與愿意錯(cuò)誤的反饋,bug被發(fā)現(xiàn),但是問題并不在你測試的代碼中。你的測試的邏輯存在一個(gè)bug,因此測試失敗了。這些bug也是您最難被檢查出來的,因?yàn)槟ǔz查您的應(yīng)用程序而不會去檢測你的測試環(huán)節(jié)。在這部分中,我會展示給你如何確認(rèn)大量的單元測試,事實(shí)上就是使得跟蹤bug變得更加容易。
代碼更加便于維護(hù)?從最終點(diǎn)考慮,你可以傾向于認(rèn)為這些信任并不是必須的,當(dāng)然你是對的,讓我們?nèi)フf,代碼中每個(gè)邏輯方法至少要有一個(gè)測試方法(當(dāng)然,你可能擁有一個(gè)以上的方法)在一個(gè)好的測試覆蓋的工程中,大概有百分之六十的代碼是能夠得到單元測試的,現(xiàn)在不得不考慮到測試也是要被維護(hù)的,如果針對一個(gè)復(fù)雜的邏輯方法你有20個(gè)測試,那么當(dāng)你向這個(gè)方法添加一個(gè)參數(shù)時(shí)會發(fā)生什么事情哪?測試無法編譯。當(dāng)你修改了類的結(jié)構(gòu)的時(shí)候同樣會發(fā)生這樣的事情。這時(shí)你突然發(fā)現(xiàn)為了能讓你的應(yīng)用程序繼續(xù)工作你自己需要改變大量的測試。當(dāng)然這會花費(fèi)你大量的時(shí)間。
為了使這個(gè)信任確認(rèn)下來,你需要確認(rèn)你的測試是便于維護(hù)的。保持DRY規(guī)則寫入:不要重復(fù)你自己。我們將更加接近的來看這個(gè)問題。
代碼更加容易被理解? 單元測試的好處通常并非是人們最初所期待的,在一個(gè)工程中考慮修改一些你之前從沒有看過的代碼(比方說,一個(gè)特殊的類或者方法).你將如何動(dòng)手處理這些代碼?你可能需要在項(xiàng)目中去瀏覽這些特定的類或者方法使用的代碼,理所當(dāng)然,單元測試就是這樣例子的一個(gè)很好的場所。同時(shí),當(dāng)正確寫入的時(shí)候,單元測試可以為工程提供一個(gè)API文件的容易讀取的設(shè)置,使得文檔的處理和代碼的理解對于整個(gè)團(tuán)隊(duì)中的新老開發(fā)者一樣的簡單,便捷。然而,這些只能在測試是易讀的和容易理解的情況下才能被確認(rèn),這個(gè)規(guī)則很多的單元測試開發(fā)者并不會遵循。我將詳述這個(gè)信任,然后在這篇文章的易讀測試的部分給你展現(xiàn)如何在去寫易讀的單元測試。
測試正確的事情
新來者在Test Driven Development (TDD)中一個(gè)最通常的錯(cuò)誤就是他們通常會搞混"Fail by testing something illogical."中的"Fail first"要求。例如,你可以用下面的規(guī)格開始這個(gè)方法:
' returns the sum of the two numbers
Function Sum(ByVal a As Integer, ByVal b As Integer) As Integer
你可以向如下的方式寫一個(gè)失敗測試:
<TestMethod()> _
Public Sub Sum_AddsOneAndTwo()
Dim result As Integer = Sum(1, 2)
Assert.AreEqual(4, result, "bad sum");
End Sub
初看上去這個(gè)處理像是一個(gè)寫失敗測試的好的方法,它完全錯(cuò)失了你寫錯(cuò)誤測試的初始點(diǎn)。
一個(gè)失敗測試驗(yàn)證了在代碼中存在一些錯(cuò)誤,當(dāng)你的測試完成后這個(gè)測試應(yīng)該是通過的,現(xiàn)在的例子中,無論如何,測試都將會失敗,即使是代碼完成,因?yàn)闇y試邏輯上不是正確的。如果希望測試通過測需要測試自身進(jìn)行修改――而不是程序的代碼的改變(當(dāng)程序代碼改變的時(shí)候,是test-first規(guī)劃的意圖)簡短來說,這個(gè)測試不會反映出程序代碼完成后的最終的結(jié)果,因此這個(gè)不是一個(gè)好的測試。
TDD中一個(gè)好的測試要求你去修改代碼,從而使它能夠按照想要的方式工作,這一點(diǎn)要?jiǎng)儆趶?qiáng)迫你去反映現(xiàn)在的真實(shí)情況或者一個(gè)非邏輯要求的渴望的結(jié)果。例如,當(dāng)1+1返回0時(shí)就意味著測試失敗。這個(gè)簡單的例子和這種情況是相似的,在練習(xí)中,如果現(xiàn)在的需求是在工作的,測試應(yīng)該可以反映你所期待的結(jié)果,然后你可以調(diào)整現(xiàn)在代碼的情況去通過這個(gè)測試。
作為一個(gè)規(guī)則,一個(gè)已經(jīng)調(diào)通的測試不應(yīng)該被移除掉,因?yàn)檫@個(gè)測試在維護(hù)工作中可以用于恢復(fù)測試。他們在你改變代碼時(shí)用來確定你沒有損害到現(xiàn)在已經(jīng)工作的函數(shù)。這就是為什么你不應(yīng)該修改那些已經(jīng)通過的測試,除非是一些很小的修改,例如增加它的可讀性(換句話說,分解測試)
當(dāng)一個(gè)測試非正常失敗? 有時(shí)你可能遇到失敗的測試,而這時(shí)你對代碼的改變是完全合理的。這通常是因?yàn)槟阌龅搅藳_突的需求。一般來說,可能是一個(gè)新的需求(一個(gè)改變的特性)與一個(gè)舊的可能已經(jīng)不再有效的需求發(fā)生了沖突。這有兩種可能:
|
1.
|
在舊的需求或者無效或者在別處測試的情況下刪除被驗(yàn)證本質(zhì)上不再有效的失敗的測試
|
|
2.
|
改變舊的測試使你可以測試新的要求(本質(zhì)上使用新的測試),然后在新的設(shè)置下(測試的邏輯狀態(tài)相同,但是初始功能函數(shù)可能有所不同)測試舊的需求。
|
而有時(shí)候一個(gè)測試在使用不完整的技術(shù)去完成任務(wù)的時(shí)候也是有效的,例如,你有一個(gè)成員類帶有一個(gè)FOO方法,它表現(xiàn)為某幾種行為,它已經(jīng)經(jīng)由Test在X年前測試完成,然后現(xiàn)在一些其他的需求加了進(jìn)來,方法的邏輯增強(qiáng)了,從而可以去處理一些類似于在獲取數(shù)據(jù)時(shí)丟失一些參數(shù)的異常處理。但這時(shí),突然Test X失敗了,雖然在測試這個(gè)函數(shù)的時(shí)候只是使用了同樣的類。這個(gè)測試的失敗是因?yàn)樵谡{(diào)用方法之前丟失了一些初始處理步驟。
這并不意味著你需要移除Test X,你將丟失對于一些重要功能的測試,這時(shí)你應(yīng)該去關(guān)心那些初始化時(shí)的問題,而不是改變類的創(chuàng)建以用來適應(yīng)你新的意圖。
當(dāng)然如果你那里有200個(gè)測試都是因?yàn)榕f的結(jié)構(gòu)導(dǎo)致的失敗,你就應(yīng)該找到這個(gè)問題來維護(hù)你的測試。這就是為什么你應(yīng)該總是移除你測試中的副本尤其是在生產(chǎn)代碼中。
測試覆蓋和測試
Angles? 你如何知道是否你的新代碼是一個(gè)好的覆蓋?當(dāng)試圖移動(dòng)一個(gè)鏈接或者一個(gè)約束檢查后,如果所有的測試依然通過,那么你就沒有足夠的代碼復(fù)制然后你可能需要添加其他的測試單元。
確認(rèn)你添加正確測試的最好方法就是測試一些最平常的行和檢查直到用非常的手段使它出錯(cuò)。這個(gè)也許很難,但是如果你不能考慮出一個(gè)讓代碼出錯(cuò)的方法,你就可能沒有好的理由在最初的地方寫下這行代碼。
你不知道什么時(shí)候下一個(gè)開發(fā)者會試圖運(yùn)行你的程序,他可能優(yōu)化或者錯(cuò)誤的刪除一些包含本質(zhì)的行。如果你沒有一個(gè)測試,它就會失敗,其他的開發(fā)者可能不會知道他們犯了錯(cuò)誤。
你也可能試圖利用一些常量去替代一些已經(jīng)通過了的測試中調(diào)用的各種各樣的參數(shù),例如,看下面的方法:
Public Function Sum(ByVal x As Integer, ByVal y As Integer, _
ByVal allowNegatives As Boolean) As Integer
If Not allowNegatives Then Throw New Exception()
Return x + y
End Function
你可以打亂代碼去測試覆蓋,這有一些關(guān)于如何測試的變化:
' Try this...
If Not True Then ' replace flag with const
If x < 0 OrElse y < 0 Then Throw New Exception()
End If
' Or this...
If Not allowNegatives Then
' replace check with const
If False OrElse y < 0 Then Throw New Exception()
End If
如果所有的測試依然通過,那么你缺少了一個(gè)測試,另外一個(gè)紅色標(biāo)志是在你為多種相同值測試的檢查。如下:
Assert.AreEqual(3, retval)
一些方法的關(guān)系只看一次(在一個(gè)測試中)意味著你可以安全的返回3作為一個(gè)值,然后所有的針對這個(gè)方法的測試都將通過,這個(gè)當(dāng)然意味著你丟失了一個(gè)測試。如果你在單元測試中檢查一下代碼,它就很容易被檢查出來。
確保你的測試寫的越簡單越好,一個(gè)單元測試一般不包括一個(gè)if switch或者其他任何的邏輯聲明。如果你發(fā)現(xiàn)你自己在你的測試中寫了一些類似于邏輯聲明的東西,這是一個(gè)好的機(jī)會來測試一個(gè)以上的事件,在做這樣的操作的時(shí)候,你會使得你的測試比讀和維護(hù)更加的困難,在生產(chǎn)代碼中同樣如此。保持你的測試簡單,你在生產(chǎn)代碼中發(fā)現(xiàn)bug要?jiǎng)儆谠谀愕膯卧獪y試中。
使測試易于運(yùn)行? 如果你的測試并不容易運(yùn)行,那么人們不會信任它。你的應(yīng)用程序最有可能有下面兩種類型的測試:
| ? |
測試在沒有任何配置的情況下平穩(wěn)的運(yùn)行(這種類型的測試,我們可以在任何的機(jī)器上,在代碼的最終版上或者在源控制上測試,并且做到?jīng)]有任何故障的測試)
|
| ? |
在運(yùn)行前需要一些配置.
|
第一種類型是你應(yīng)該模仿的,第二種類型是你通常做的,尤其你如果你是一個(gè)新的單元測試。如果你發(fā)現(xiàn)你自己測試時(shí)有很多的特殊的需求,現(xiàn)在是正常的,但是重要的一點(diǎn)就是你要隔離出兩個(gè)組讓他們能夠單獨(dú)的去做測試。
我們的想法是任意一個(gè)開發(fā)者都應(yīng)該有能力修改和運(yùn)行一些不需要設(shè)置特殊的配置的測試進(jìn)行測試。如果這有一些測試需要在運(yùn)行前有特殊的關(guān)注,開發(fā)者應(yīng)該知道他們,然后他可以花一些時(shí)間學(xué)習(xí)這些測試的方法。因?yàn)楹芏嗟拈_發(fā)者比較的懶(當(dāng)然,不是你),你可以設(shè)想,他們不會去做那些特殊的設(shè)置,相反,他們會讓測試失敗因?yàn)樗麄冇懈玫氖虑槿プ觥?/p>
當(dāng)用戶讓測試失敗時(shí),他們開始考慮他們不能夠信任這些測試了。很難說是否測試可以在一個(gè)中找到一個(gè)正式的bug或者只是一個(gè)錯(cuò)誤的定位。開發(fā)者可能不明白為什么測試者會在一開始就執(zhí)行失敗。一旦他們不再信任你的測試,開發(fā)者將會停止運(yùn)行它們,那么bug就會駐留在程序中,之后一連串的麻煩就來了。。。
為了避免這件事情,確認(rèn)你總是有一個(gè)組準(zhǔn)備好了去測試,測試程序則是可以安全運(yùn)行,可信任的。把那些屬于配置挑戰(zhàn)組的測試放到不同的文件夾,樹或者工程中,同時(shí)標(biāo)記特殊的說明指明他們在運(yùn)行前需要做什么。完成這些后,開發(fā)者可以不投入時(shí)間去配置就開始測試工作。當(dāng)他們有時(shí)間和需要時(shí),他們也可以配置,運(yùn)行更多的測試環(huán)節(jié)。
創(chuàng)建維護(hù)測試
我們應(yīng)該試著避免測試私有或保護(hù)成員。這篇文章也許能夠幫助一些人解決一部分問題,但是我很堅(jiān)決相信百分之九十九的時(shí)間,你可以全面的測試一個(gè)類,通過編寫一些與它的獨(dú)立公共接口相反的單元測試。測試私有成員可以使你的測試更加脆弱,如果這個(gè)需要被測試的類的一些內(nèi)在方面略有改動(dòng)的話。你應(yīng)該使用通過調(diào)用一些代碼里別處的公共功能這一方法去測試私有功能。當(dāng)你依然能夠確定全部功能并沒有改變的時(shí)候,僅僅測試公共成員會導(dǎo)致測試遭受常量代碼的因式分解以及內(nèi)部的執(zhí)行情況改變。
在可能的時(shí)候,應(yīng)該重新使用你的創(chuàng)造物,處理過程,和聲明代碼。不要在一個(gè)單元測試中直接的創(chuàng)建類的實(shí)例。如果你在任何并不包含在此單元測試框架中的類前面看到這個(gè)單詞“new”,你應(yīng)該考慮一下將你創(chuàng)造的代碼放在一個(gè)特殊的整體方法之中,它可以為你創(chuàng)建一個(gè)對象實(shí)例。你可以到時(shí)再重新使用這個(gè)方法來獲得你的測試在其他測試之中的最新實(shí)例。這樣可以幫助你來保持這個(gè)測試維護(hù)所需的時(shí)間,然后在測試進(jìn)行的時(shí)候,從對代碼無法預(yù)料的改變之中保護(hù)你的測試。作為一個(gè)例子,Figure?1展示了一對簡單的測試,它使用了一個(gè)Calc類。
假設(shè)你有20,或者你甚至有100,與Calc類做相反測試,所有這些看起來令人吃驚的相似。現(xiàn)在一個(gè)計(jì)劃的改變迫使你不得不刪除默認(rèn)的Calc構(gòu)造器并且使用一個(gè)含有一些參數(shù)的不同的構(gòu)造器。馬上,你所有的測試就被暫停了。你可能可以很輕易的發(fā)現(xiàn)問題的關(guān)鍵并修復(fù)它,但你也可能做不到。最主要的問題是你將會浪費(fèi)很多寶貴時(shí)間在修理你的測試上面。如果你在你的測試類之中使用一個(gè)整體的方法去創(chuàng)建Calc 實(shí)例,就像Figure?2所顯示的那樣,這些就并不是個(gè)問題。
我已經(jīng)對測試做了一些改變已使它們能夠具有更多可維護(hù)性。首先,我將新創(chuàng)建的代碼遷移至可以再度使用的整體方法之中。這就意味著我只需僅僅改變一個(gè)簡單的方法以使得在這個(gè)測試類中的所有測試在一個(gè)新的構(gòu)造器中的能夠正常的工作。另外一個(gè)為創(chuàng)造問題而設(shè)的簡單解決方法是把創(chuàng)作物遷移到測試類的<TestInitialize()>方法之中。不幸運(yùn)的是,這個(gè)能夠很好的工作僅僅在你重新使用一個(gè)對象并在一些測試中把它當(dāng)作一個(gè)局部類變量。如果你僅僅為一些測試使用它(部分相關(guān)成員),你倒不如在測試中將它們實(shí)例化,并且使它們更具易讀性。
順便一提的是,請注意,我已經(jīng)將方法命名為Factory_CreateDefaultCalc 。我很喜歡將我測試中的任何幫助方法用特殊的前綴來命名,這樣我就能很輕易的掌握它是做什么用的。這樣對易讀性也是非常有幫助的。
我的第二個(gè)改變是重新使用測試中的聲明代碼,并將這段代碼遷移到一個(gè)確認(rèn)方法之中。所謂確認(rèn)方法是你測試中的一個(gè)可再度使用的方法, 這個(gè)方法包含了一個(gè)聲明語句但是它可以接受不同輸入和在輸入的基礎(chǔ)上進(jìn)行校驗(yàn)。當(dāng)你在不同輸入或者不同的初始狀態(tài)下一次又一次的聲明同一事物時(shí),你可以使用確認(rèn)方法。這一方法的優(yōu)點(diǎn)是既使在一個(gè)不同的方法里面聲明,如果這個(gè)聲明失敗了你將可以繼續(xù)保有一個(gè)異常處理,而且原始調(diào)用測試將會顯示在測試失敗輸出窗口之中。
我也在Calc 中傳遞實(shí)例而不是使用一個(gè)局部變量,因此我知道我經(jīng)常傳遞一個(gè)實(shí)例,而且這個(gè)實(shí)例是調(diào)用測試將其初始化的。當(dāng)你想要改變對象狀態(tài)時(shí)你可能想要做同樣的事情,舉個(gè)例子來說,當(dāng)在測試下或者在將會傳遞給測試的對象下配置特殊對象時(shí),可以使用特殊的Configure_XX方法。這些方法應(yīng)該能夠解釋他們配置一個(gè)對象將會用來做什么用。Figure?3之中的代碼就是以上方法的實(shí)例。
這個(gè)測試擁有很多設(shè)置代碼可以用來處理向注冊管理器對象中添加初始狀態(tài),它是這個(gè)測試類之中的成員。在此的確也有一些重復(fù)。Figure?4顯示了在初始代碼之外這些事例在因式分解之后將會如何變化。
修訂測試具有非常高的可讀性和穩(wěn)定性。僅僅需要注意的是不要那么的refactor你的測試,他們可能會以一個(gè)單一的,不可讀的代碼行作為結(jié)束。應(yīng)該注意的是我在這里可能依然使用一個(gè)Verify_XX 方法,但是這并不是我真正要在這里加以說明的。
消除測試之間的依賴關(guān)系
一個(gè)測試應(yīng)該能夠自我獨(dú)立。它不應(yīng)該與其他測試相關(guān)聯(lián),也不應(yīng)該依賴任何具有特殊運(yùn)行順序的測試,它應(yīng)該能夠獲得你所寫的所有測試,可以隨意運(yùn)行所有測試或者只運(yùn)行其中的一部分,并且是以任何順序,而且要能夠確保它們無論怎樣都應(yīng)該正確的運(yùn)行。如果你不能夠執(zhí)行這個(gè)規(guī)則,你將會只在某種特殊的情況下按照預(yù)期的表現(xiàn)來運(yùn)行的狀況下結(jié)束你的測試。這樣子的話,當(dāng)你在最終期限下與此同時(shí)你還想確定你沒有向系統(tǒng)之中引進(jìn)新的問題的時(shí)候,當(dāng)然就會出現(xiàn)問題。你可能很困惑而且考慮著是不是你的代碼出現(xiàn)問題,這時(shí),在事實(shí)上,問題其實(shí)僅僅是你的測試運(yùn)行順序所引起的。因此,你可能開始錯(cuò)過了一些在測試中失敗的結(jié)果而且使它越寫越少。這將會是個(gè)長期的過程。
如果你從一個(gè)測試調(diào)出至另一個(gè)測試之中,你應(yīng)該在它們之間創(chuàng)建一個(gè)從屬關(guān)系。你本質(zhì)上說是在一個(gè)測試中測試兩個(gè)事物(我將會在下一章中解釋為什么這會成為一個(gè)問題)。就另一方面來說,如果你有測試B,它與測試A 所產(chǎn)生的狀態(tài)是不相關(guān)的,那么你會陷入“順序”陷阱之中。如果你或者其他人想要改變測試A,測試B將會暫停而且你不知它暫停的原因。對這些故障進(jìn)行故障處理會浪費(fèi)很多時(shí)間。
使用<TestInitialize()> 和<TestCleanup()>方法是本質(zhì)上能夠獲得更好的測試隔離。確定你的測試數(shù)據(jù)時(shí)刻是最新的,而且測試下對象的也具有新的實(shí)例,而且所有的狀態(tài)可以提前預(yù)知,而且無論你的測試在任何地方或者任何時(shí)間被運(yùn)行,運(yùn)行的情況都是相同的。
在一個(gè)單獨(dú)單元測試中避免多重聲明
我們將聲明故障看作一個(gè)程序弊病的象征且聲明被當(dāng)作軟件體的指示點(diǎn)或者“血液檢查”。你可以找到越多的癥狀,程序弊病就越可以輕松的被診斷和排除掉。如果你在一個(gè)測試中定義了多重聲明,只有第一個(gè)故障聲明將會以拋出異常的方式顯示出來。請參考下面插圖之中的測試代碼:
<TestMethod()> _
Public Sub Sum_AnyParamBiggerThan1000IsNotSummed()
Assert.AreEqual(3, Sum(1001, 1, 2)
Assert.AreEqual(3, Sum(1, 1001, 2) ' Assert fails
Assert.AreEqual(3, Sum(1, 2, 1001) ' This line never executes
End Sub
你可能沒有發(fā)現(xiàn)以上代碼之中其他可能的征兆。在一個(gè)故障之后,并發(fā)的聲明不會被執(zhí)行。這些不能生效的聲明可能提供了有價(jià)值的數(shù)據(jù)(或者征兆)可能能夠幫助你很快的集中的焦點(diǎn)而且發(fā)現(xiàn)潛在的問題。因此在一個(gè)獨(dú)立的測試中運(yùn)行多重聲明增加了具有很少價(jià)值復(fù)雜性。另外,聲明應(yīng)該被獨(dú)立的運(yùn)行,我們應(yīng)該設(shè)置自我獨(dú)立的單元測試以使得你具有能夠很好的發(fā)現(xiàn)錯(cuò)誤的機(jī)會。
創(chuàng)建易讀性測試
如果你以前寫過單元測試,你是否在單元測試上寫了一個(gè)好的聲明行?可許不是這樣的,大多數(shù)開發(fā)者并不厭煩去寫一個(gè)好的聲明因?yàn)樗麄兏雨P(guān)心去寫測試。
假設(shè)你是團(tuán)隊(duì)中的一個(gè)新的開發(fā)者,你試圖讀一個(gè)單元測試。連接這個(gè):
<TestMethod()> _
Public Sub TestCalcParseNegative()
Dim c As New Calc
Assert.AreEqual(1000, c.Parse("-1, -1000")
End Sub
作為一個(gè)簡單的練習(xí),如果你理解了上例中Calc分列方法的用法,你很可能可以進(jìn)行很好的推測,但是他可以簡單的作為人員數(shù)量的用例使得輸出結(jié)果為1000:
| ? |
在組中返回最大的負(fù)數(shù)作為一個(gè)正數(shù)。
|
| ? |
如果數(shù)字是負(fù)數(shù)且返回值為剩下幾個(gè)數(shù)的總和作為一個(gè)正數(shù),那么忽略第一個(gè)數(shù)字。
|
| ? |
返回相互作乘積運(yùn)算而得的數(shù)字。
|
現(xiàn)在請參考下面在單元測試之中的小改動(dòng):
<TestMethod()> _
Public Sub Parse_NegativeFirstNum_ReturnsSumOfTheRestAsPositive()
Dim c As New Calc
Dim parsedSumResult As Integer = c.Parse("-1", "-1000")
Const SUM_WITH_IGNORED_FIRST_NUM As Integer = 1000
Assert.AreEqual(SUM_WITH_IGNORED_FIRST_NUM, parsedSumResult)
End Sub
這個(gè)是不是比較容易理解呢?當(dāng)聲明消息消失之后,表達(dá)意圖最合適的地方就是測試的名字。 如果你廣泛的使用了它,你將會發(fā)現(xiàn)你不再需要讀測試代碼就能明白代碼測試的目的所在。事實(shí)上,你經(jīng)常根本不需要寫任何注釋,因?yàn)榇a,特別是那些帶著實(shí)例的,他們自己是證明自己的。
名字包含了三部分內(nèi)容: 測試下方法的名字(解析),測試下的狀態(tài)或者規(guī)則(帶著第一個(gè)負(fù)數(shù)傳遞一個(gè)字符串),以及預(yù)期的輸出或者運(yùn)行情況(剩余數(shù)字的總和以一個(gè)正數(shù)的形式返回)。需要注意的是我從名稱中將Test以及Calc這兩個(gè)詞刪除。我已經(jīng)知道這是一個(gè)屬性的測試因此在此沒有重復(fù)此信息的必要。我也知道這是一個(gè)在Calc類中的測試因?yàn)闇y試類經(jīng)常是寫給一個(gè)特殊類的(這個(gè)類也許已經(jīng)被命名為CalcTests)。
名字也許會很長,但是又有誰在乎呢?它讀起來更像是一個(gè)標(biāo)準(zhǔn)英語的句子而且它使得一個(gè)新來的開發(fā)者更容易明白測試的內(nèi)容。更是這樣,當(dāng)這個(gè)測試發(fā)生故障時(shí),我們甚至不需要調(diào)試代碼就可以知道問題究竟出在哪里。
需要注意的是,我已經(jīng)在前面分別實(shí)際演示了通過在不同行中創(chuàng)建一個(gè)結(jié)果變量的方法從聲明操作中進(jìn)行分解操作。這樣做至少有兩個(gè)理由。第一個(gè)理由是,你可以為一個(gè)變量分配一個(gè)可讀性強(qiáng)的名字,它可以包含結(jié)果,這樣可以使你的聲明行非常易于理解以及易于讀。第二點(diǎn)是,測試下與對象相反的invocation 可能非常的長,它可能會使你的聲明行延伸出屏幕的邊緣之外,這樣導(dǎo)致測試者向右滾屏。就我個(gè)人而言,我認(rèn)為這個(gè)是非常惱人的。
我在我的測試中使用了很多常量以確保我的聲明讀起來像一本書。在先前的例子之中,你可以讀到聲明中說:“確保分解總數(shù)是與忽略第一個(gè)數(shù)后所得總和是相等的。” 為你的變量取一個(gè)很好的名字能夠在某些程度上彌補(bǔ)對于測試的命名不足。
當(dāng)然,有時(shí)一個(gè)聲明 消息是在一個(gè)單元測試中傳遞intent的最好的方法。 一個(gè)好的聲明消息始終能夠解釋什么因該會發(fā)生或者什么發(fā)生了而且為什么會出錯(cuò)。舉個(gè)例子來說,“分列應(yīng)該忽略掉第一個(gè)數(shù)字如果這個(gè)數(shù)字是個(gè)負(fù)數(shù)的話”,“分列不能夠忽略掉第一個(gè)負(fù)數(shù)”,還有“X調(diào)用對象Y標(biāo)記錯(cuò)誤”這些都是有用的聲明消息,它們很清晰的描述了結(jié)果的情況。
在你的設(shè)置方法中避免部分相關(guān)的代碼
一個(gè)<TestInitialize()> 方法是樣例成員變量在測試中使用的一個(gè)好地方。你所有的測試,只有在一部分的測試中避免變量。他們可以為測試設(shè)置本地變量。如果你創(chuàng)建了部分相關(guān)的實(shí)例作為類的成員,用來在測試中簡單的避免創(chuàng)建的副本,你應(yīng)該使用在文章前面解釋的工廠方法,使用部分相關(guān)變量使得你的代碼和設(shè)置方法缺少易讀性。一旦變量在一個(gè)或者每個(gè)測試中使用,那么他應(yīng)該是<TestInitialize()> 方法的一個(gè)成員和變量。
Figure?5
展現(xiàn)了一個(gè)擁有兩個(gè)成員變量的類的測試。但是他們中的一個(gè)(cxNum)只被部分使用。Figure?6 展現(xiàn)了如何在測試中替換代碼從而使它更加易讀的方法。
總結(jié)
就像你所看到的,寫單元測試并不是一個(gè)微不足道的任務(wù),如果步驟正確,單元測試可以為開發(fā)者的生產(chǎn)力和代碼的質(zhì)量帶來令人驚訝的提高,他可以幫助你去創(chuàng)建的應(yīng)用程序含有更少的錯(cuò)誤,同時(shí)也可以便于其他的開發(fā)者去洞察你的代碼,但是他也需要在之前承擔(dān)一個(gè)義務(wù),確認(rèn)遵循一些簡單的規(guī)則。當(dāng)方法并不是很好時(shí),單元測試則可能達(dá)到一個(gè)相反的結(jié)果,從而浪費(fèi)您的時(shí)間,并且使測試過程更加復(fù)雜。
Roy Osherove Agile組的負(fù)責(zé)人, 這個(gè)顧問公司致力于agile software development 和 .NET architecture的研究工作. Roy同時(shí)維護(hù)了一個(gè)blog在 www.iserializable.com上有相關(guān)的信息. 你可以通過Email聯(lián)系他: Roy@TeamAgile.com.
轉(zhuǎn)到英文頁面