返回頁首
創建維護測試
我們應該試著避免測試私有或保護成員。這篇文章也許能夠幫助一些人解決一部分問題,但是我很堅決相信百分之九十九的時間,你可以全面的測試一個類,通過編寫一些與它的獨立公共接口相反的單元測試。測試私有成員可以使你的測試更加脆弱,如果這個需要被測試的類的一些內在方面略有改動的話。你應該使用通過調用一些代碼里別處的公共功能這一方法去測試私有功能。當你依然能夠確定全部功能并沒有改變的時候,僅僅測試公共成員會導致測試遭受常量代碼的因式分解以及內部的執行情況改變。
在可能的時候,應該重新使用你的創造物,處理過程,和聲明代碼。不要在一個單元測試中直接的創建類的實例。如果你在任何并不包含在此單元測試框架中的類前面看到這個單詞“new”,你應該考慮一下將你創造的代碼放在一個特殊的整體方法之中,它可以為你創建一個對象實例。你可以到時再重新使用這個方法來獲得你的測試在其他測試之中的最新實例。這樣可以幫助你來保持這個測試維護所需的時間,然后在測試進行的時候,從對代碼無法預料的改變之中保護你的測試。作為一個例子,Figure 1展示了一對簡單的測試,它使用了一個Calc類。
假設你有20,或者你甚至有100,與Calc類做相反測試,所有這些看起來令人吃驚的相似,F在一個計劃的改變迫使你不得不刪除默認的Calc構造器并且使用一個含有一些參數的不同的構造器。馬上,你所有的測試就被暫停了。你可能可以很輕易的發現問題的關鍵并修復它,但你也可能做不到。最主要的問題是你將會浪費很多寶貴時間在修理你的測試上面。如果你在你的測試類之中使用一個整體的方法去創建Calc 實例,就像Figure 2所顯示的那樣,這些就并不是個問題。
我已經對測試做了一些改變已使它們能夠具有更多可維護性。首先,我將新創建的代碼遷移至可以再度使用的整體方法之中。這就意味著我只需僅僅改變一個簡單的方法以使得在這個測試類中的所有測試在一個新的構造器中的能夠正常的工作。另外一個為創造問題而設的簡單解決方法是把創作物遷移到測試類的<TestInitialize()>方法之中。不幸運的是,這個能夠很好的工作僅僅在你重新使用一個對象并在一些測試中把它當作一個局部類變量。如果你僅僅為一些測試使用它(部分相關成員),你倒不如在測試中將它們實例化,并且使它們更具易讀性。
順便一提的是,請注意,我已經將方法命名為Factory_CreateDefaultCalc 。我很喜歡將我測試中的任何幫助方法用特殊的前綴來命名,這樣我就能很輕易的掌握它是做什么用的。這樣對易讀性也是非常有幫助的。
我的第二個改變是重新使用測試中的聲明代碼,并將這段代碼遷移到一個確認方法之中。所謂確認方法是你測試中的一個可再度使用的方法, 這個方法包含了一個聲明語句但是它可以接受不同輸入和在輸入的基礎上進行校驗。當你在不同輸入或者不同的初始狀態下一次又一次的聲明同一事物時,你可以使用確認方法。這一方法的優點是既使在一個不同的方法里面聲明,如果這個聲明失敗了你將可以繼續保有一個異常處理,而且原始調用測試將會顯示在測試失敗輸出窗口之中。
我也在Calc 中傳遞實例而不是使用一個局部變量,因此我知道我經常傳遞一個實例,而且這個實例是調用測試將其初始化的。當你想要改變對象狀態時你可能想要做同樣的事情,舉個例子來說,當在測試下或者在將會傳遞給測試的對象下配置特殊對象時,可以使用特殊的Configure_XX方法。這些方法應該能夠解釋他們配置一個對象將會用來做什么用。Figure 3之中的代碼就是以上方法的實例。
這個測試擁有很多設置代碼可以用來處理向注冊管理器對象中添加初始狀態,它是這個測試類之中的成員。在此的確也有一些重復。Figure 4顯示了在初始代碼之外這些事例在因式分解之后將會如何變化。
修訂測試具有非常高的可讀性和穩定性。僅僅需要注意的是不要那么的refactor你的測試,他們可能會以一個單一的,不可讀的代碼行作為結束。應該注意的是我在這里可能依然使用一個Verify_XX 方法,但是這并不是我真正要在這里加以說明的。
消除測試之間的依賴關系
一個測試應該能夠自我獨立。它不應該與其他測試相關聯,也不應該依賴任何具有特殊運行順序的測試,它應該能夠獲得你所寫的所有測試,可以隨意運行所有測試或者只運行其中的一部分,并且是以任何順序,而且要能夠確保它們無論怎樣都應該正確的運行。如果你不能夠執行這個規則,你將會只在某種特殊的情況下按照預期的表現來運行的狀況下結束你的測試。這樣子的話,當你在最終期限下與此同時你還想確定你沒有向系統之中引進新的問題的時候,當然就會出現問題。你可能很困惑而且考慮著是不是你的代碼出現問題,這時,在事實上,問題其實僅僅是你的測試運行順序所引起的。因此,你可能開始錯過了一些在測試中失敗的結果而且使它越寫越少。這將會是個長期的過程。
如果你從一個測試調出至另一個測試之中,你應該在它們之間創建一個從屬關系。你本質上說是在一個測試中測試兩個事物(我將會在下一章中解釋為什么這會成為一個問題)。就另一方面來說,如果你有測試B,它與測試A 所產生的狀態是不相關的,那么你會陷入“順序”陷阱之中。如果你或者其他人想要改變測試A,測試B將會暫停而且你不知它暫停的原因。對這些故障進行故障處理會浪費很多時間。
使用<TestInitialize()> 和<TestCleanup()>方法是本質上能夠獲得更好的測試隔離。確定你的測試數據時刻是最新的,而且測試下對象的也具有新的實例,而且所有的狀態可以提前預知,而且無論你的測試在任何地方或者任何時間被運行,運行的情況都是相同的。
在一個單獨單元測試中避免多重聲明
我們將聲明故障看作一個程序弊病的象征且聲明被當作軟件體的指示點或者“血液檢查”。你可以找到越多的癥狀,程序弊病就越可以輕松的被診斷和排除掉。如果你在一個測試中定義了多重聲明,只有第一個故障聲明將會以拋出異常的方式顯示出來。請參考下面插圖之中的測試代碼:
<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 |
你可能沒有發現以上代碼之中其他可能的征兆。在一個故障之后,并發的聲明不會被執行。這些不能生效的聲明可能提供了有價值的數據(或者征兆)可能能夠幫助你很快的集中的焦點而且發現潛在的問題。因此在一個獨立的測試中運行多重聲明增加了具有很少價值復雜性。另外,聲明應該被獨立的運行,我們應該設置自我獨立的單元測試以使得你具有能夠很好的發現錯誤的機會。
返回頁首
創建易讀性測試
如果你以前寫過單元測試,你是否在單元測試上寫了一個好的聲明行?可許不是這樣的,大多數開發者并不厭煩去寫一個好的聲明因為他們更加關心去寫測試。
假設你是團隊中的一個新的開發者,你試圖讀一個單元測試。連接這個:
<TestMethod()> _ Public Sub TestCalcParseNegative() Dim c As New Calc Assert.AreEqual(1000, c.Parse("-1, -1000") End Sub |
作為一個簡單的練習,如果你理解了上例中Calc分列方法的用法,你很可能可以進行很好的推測,但是他可以簡單的作為人員數量的用例使得輸出結果為1000:
在組中返回最大的負數作為一個正數。
如果數字是負數且返回值為剩下幾個數的總和作為一個正數,那么忽略第一個數字。
返回相互作乘積運算而得的數字。
現在請參考下面在單元測試之中的小改動:
<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 |
這個是不是比較容易理解呢?當聲明消息消失之后,表達意圖最合適的地方就是測試的名字。 如果你廣泛的使用了它,你將會發現你不再需要讀測試代碼就能明白代碼測試的目的所在。事實上,你經常根本不需要寫任何注釋,因為代碼,特別是那些帶著實例的,他們自己是證明自己的。
名字包含了三部分內容: 測試下方法的名字(解析),測試下的狀態或者規則(帶著第一個負數傳遞一個字符串),以及預期的輸出或者運行情況(剩余數字的總和以一個正數的形式返回)。需要注意的是我從名稱中將Test以及Calc這兩個詞刪除。我已經知道這是一個屬性的測試因此在此沒有重復此信息的必要。我也知道這是一個在Calc類中的測試因為測試類經常是寫給一個特殊類的(這個類也許已經被命名為CalcTests)。
名字也許會很長,但是又有誰在乎呢?它讀起來更像是一個標準英語的句子而且它使得一個新來的開發者更容易明白測試的內容。更是這樣,當這個測試發生故障時,我們甚至不需要調試代碼就可以知道問題究竟出在哪里。
需要注意的是,我已經在前面分別實際演示了通過在不同行中創建一個結果變量的方法從聲明操作中進行分解操作。這樣做至少有兩個理由。第一個理由是,你可以為一個變量分配一個可讀性強的名字,它可以包含結果,這樣可以使你的聲明行非常易于理解以及易于讀。第二點是,測試下與對象相反的invocation 可能非常的長,它可能會使你的聲明行延伸出屏幕的邊緣之外,這樣導致測試者向右滾屏。就我個人而言,我認為這個是非常惱人的。
我在我的測試中使用了很多常量以確保我的聲明讀起來像一本書。在先前的例子之中,你可以讀到聲明中說:“確保分解總數是與忽略第一個數后所得總和是相等的! 為你的變量取一個很好的名字能夠在某些程度上彌補對于測試的命名不足。
當然,有時一個聲明 消息是在一個單元測試中傳遞intent的最好的方法。 一個好的聲明消息始終能夠解釋什么因該會發生或者什么發生了而且為什么會出錯。舉個例子來說,“分列應該忽略掉第一個數字如果這個數字是個負數的話”,“分列不能夠忽略掉第一個負數”,還有“X調用對象Y標記錯誤”這些都是有用的聲明消息,它們很清晰的描述了結果的情況。
返回頁首
在你的設置方法中避免部分相關的代碼
一個<TestInitialize()> 方法是樣例成員變量在測試中使用的一個好地方。你所有的測試,只有在一部分的測試中避免變量。他們可以為測試設置本地變量。如果你創建了部分相關的實例作為類的成員,用來在測試中簡單的避免創建的副本,你應該使用在文章前面解釋的工廠方法,使用部分相關變量使得你的代碼和設置方法缺少易讀性。一旦變量在一個或者每個測試中使用,那么他應該是<TestInitialize()> 方法的一個成員和變量。
Figure 5 展現了一個擁有兩個成員變量的類的測試。但是他們中的一個(cxNum)只被部分使用。Figure 6 展現了如何在測試中替換代碼從而使它更加易讀的方法。
返回頁首
總結
就像你所看到的,寫單元測試并不是一個微不足道的任務,如果步驟正確,單元測試可以為開發者的生產力和代碼的質量帶來令人驚訝的提高,他可以幫助你去創建的應用程序含有更少的錯誤,同時也可以便于其他的開發者去洞察你的代碼,但是他也需要在之前承擔一個義務,確認遵循一些簡單的規則。當方法并不是很好時,單元測試則可能達到一個相反的結果,從而浪費您的時間,并且使測試過程更加復雜。
文章來源于領測軟件測試網 http://www.kjueaiud.com/