單元測試和編碼協作

文出處:             J u l y / A u g u s t 2 0 0 5 IEEE SOFTWARE
正文翻譯:
    在缺陷和錯誤發生的時候就發現並糾正它們對任何快速、控制成本的軟件項目來說是很關鍵的。由代碼編寫者實施的單元測試(Unit Test)是最常用的方法。本文的作者Panagiotis Louridas總結了他在最流行的Java單元測試OSS(開放源代碼軟件)工具——JUnit上的經驗。
    我們期待任何來自您的迴音。
<!--[if !supportLists]-->         <!--[endif]-->Christof Ebor
    自1947年9月9日一隻飛蛾被發現困在Harvard Mark Ⅱ中開始,調試問題就一直困擾着程序員。(這第一個“真實的Bug”,至今還存放在Smithsonian的美國國家歷史博物館)。計算機先驅Manrice Wilkes在談到他意識到調試的嚴重性的時候說道:“我生命中餘下的時間,將有很大的一部分將被用於從我自己所寫的代碼中查找錯誤。”他的這個解釋在很多的計算機專家身上得到了驗證。
    單元測試是一個檢測bug的方法。首先,單元測試適用於單獨的代碼片斷。例如函數,方法。除非這些最小的代碼片斷是正確的,否則,整個軟件的“大廈“將會倒塌。其次,單元測試讓代碼的編寫者也同時來參與代碼的測試。通過測試能夠查出bug,但程序員除了編碼不願花費時間在其他任何事上。
JUnit是一個開放源代碼的Java類庫,目的是讓單元測試更簡單、有趣。事實上JUnit是如此的有趣以至於程序員會愛上編寫測試代碼。Kent Beck和Erich Gamma從Beck的SmallTalk測試框架中獲得了靈感並建立了JUnit項目。最初的想法是讓編碼和測試能同時進行。程序員寫若干行的代碼,然後爲這幾行的代碼編寫測試。如果沒有通過測試,就改正程序中的bug;如果正常通過測試,就繼續編碼和測試。所有的測試碼隨着代碼的變化而變化,一旦運行代碼發生變化,通過迴歸測試的方法能保證整個軟件不會因爲這些變化而遭到破壞。 
編碼和測試的整合是極限編程XP(eXtreme Programming)和敏捷軟件開發活動的中心環節。在敏捷軟件開發過程中,軟件通過不斷增加實現功能來構建,簡言之就是活動的突發催生軟件功能的完善。)爲了達到這個目的則必須保證所有已經完成的代碼都是正確的,只有這樣纔能有自信在這些代碼的基礎上進行後續開發。
一、JUnit簡介
    在單元測試中,我們經常編寫這樣的代碼:按實際需求提供輸入的代碼和以輸出結果形式展示的代碼的運行情況。程序員長期使用的一個簡單的方法是:編寫一系列的if條件語句與預期的結果來進行比較。在JUnit中,我們並不需要這些if語句,而是寫斷言(assertion)。斷言是這樣一個方法:預先標誌出期望的結果,並將得到的結果與之進行比較,如果匹配,則斷言成功,否則斷言失敗。
    一般的說,測試需要建立“測試框架”:例如初始化變量,創建對象等。在JUnit中,準備工作被稱爲裝配(setup)。setup和assertions是相互獨立的,所以相同的測試框架可適用於若干獨立的測試。setup過程在測試以前執行。
    而當一個測試結束的時候,可能需要對測試框架進行一些清理活動。在JUnit中,清理活動被稱爲拆卸(TearDown)。它保證每一個測試不會留下任何的影響。如果接下來有另一個測試活動開始了,那麼這個測試的setup過程就能被正確得執行。setup和teardown被成爲一個測試的固定環節
    單獨的測試被稱爲測試用例(Test Cases),測試用例幾乎不存在於真空中。如果一個項目經歷了若干個單元測試,就能積累一定數量的測試用例集。程序員往往將這些測試一起運行。如果測試一起運行,就將測試聯合成爲測試組(test suites)。這種情況下,程序員將多個測試用例組合成爲一個測試組(test suites),作爲一個單獨的整體來運行。
二、一個例子
    用實踐來理解JUnit往往更簡單。設想我們需要一個Complex類(當然不是從其他地方得到現成的)。這個Complex類應當包含常規的複數運算和操作,我們會對這些方法進行測試。
首先,新建一個TestCase子類。在這個子類中,爲fixture的每一部分(setup和teardown)新建實例變量。然後重寫setUp()(注意保留)方法以初始化變量,重寫tearDown()方法以清理所有測試過程中使用的資源從而避免任何的副作用。在這個例子中,所有的操作並沒有副作用,所以我們並不需要重寫tearDown()方法。Figure 1(a)中展示瞭如何創建一個TestCase子類並重寫setUp()方法。
a
       import junit.framework.TestCase;
       import junit.framework.Test;
       import junit.framework.TestSuite;
       public class ComplexTest extends TestCase {
              private Complex a;
              private Complex b;
              protected void setUp() {
                     a = new Complex(1, -1);
                     b = new Complex(2, 5);
              }
       }
       b
       public void testComplexEquality()
              Complex expected = new Complex(1, -1);
              assertEquals(expected, a);
       }
       public void testComplexAddition() {
              Complex expected = new Complex(3, 4);
              assertEquals(expected, a.add(b));
       }
       public void testComplexMultiplication() {
              Complex expected = new Complex(1*2 - (-1)*5,1*5 + (-1)*2);
              assertEquals(expected, a.multiply(b));
       }
c
       public static Testsuite() {
              return new TestSuite(ComplexTest.class);
      }
Figure 1 一個複數類例子:(a)JUnit測試的setup,(b)JUnit測試的實例,(c)在一個類中爲所有實例動態得創建一個測試組(test suite)
 
要創建測試用例,我們要在ComplexTest類中添加包含我們要執行的assertion的方法。測試方法名以“test”開始,並且必須是public修飾的(public 類型)。只有這樣JUnit才能通過Java的反射機制來調用它。我們接下來要測試的是對象的判等在我們的Complex類中重寫了java.lang.Object的equals()方法,加法和乘法。
    Figure 1(b)展示了上述幾個測試方法的實現。
    我們可以用任何的Java原始類中都定義的assertEquals方法。(The assertEquals method is overloaded and defined for all wuhuif primitives, apart from wuhuif objects.這裏的assertEquals()方法重寫和定義了所有Java原始類中的定義。)也可以用assertTrue和assertFalse來測試條件;用assertNull和assertNotNull來判斷是否爲空的引用;assertSame和assertNotSame來測試兩個對象是否指向同一個引用。
    接下來就要開始運行測試了。最簡單的方法就是讓JUnit通過反射機制來找到預定義的測試用例。當然也可以通過編寫一些其他的代碼來靜態指定要使用的測試用例。要用反射機制來動態指定要運行的測試用例,我們只需要像Figure 1(c)一樣在ComplexTest類中添加一個Testsuite()方法。
三、運行測試
    運行測試時,我們不需要爲Complex類添加任何一行代碼。事實上,在以測試驅動的開發過程中,是先編寫測試,然後寫具體的類的實現。當然一開始就編寫類的代碼也不會有任何的問題。JUnit的相關信息可以在www.JUnit.org上找到。這個站點中有很多文檔,包括Beck和Gamma原創的介紹JUnit的文章。JUnit的安裝非常簡單,只需要解壓就完成了。在編譯的過程中,需要確保ClassPath中包含了JUnit所在的目錄。假設你將程序放在/usr/java/JUnit下,編譯的命令就是javac –classpath “.:/usr/java/JUnit/JUnit.jar” ComplexTest.java。
    要運行測試,可以通過一下命令啓動JUnit的圖形界面:java -classpath “.:/usr/java/JUnit/JUnit.jar” JUnit.swingui.TestRunner 然後輸入包含測試組的類名。接下來JUnit就會進行測試,如果所有的測試都通過了,窗口上就會出現一個綠色的滾動條,如Figure2所示(圖略)。由此也就有了JUnit的箴言:“Keep the bargreen to keep the code clean.”,意思爲,保持條爲綠色,也就得到了良好的代碼。如果出現了錯誤,條就會變稱紅色,同時會出現一個關於失敗測試的詳細信息。如果你更喜歡文本界面,可以通過如下命令調用:
wuhuif -classpath “.:/usr/java/JUnit/JUnit.jar” JUnit.textui. TestRunner ComplexTest,在中端窗口中就會出現運行結果。
    現在,你可以爲Complex類添加更多的方法,比如一個複數的除法。與此同時,你也要爲ComplexTest類添加一個測試用例,運行測試,然後糾正任何存在的錯誤,繼續編碼和測試。
四、JUnit的遺贈和競爭
    JUnit的成功激勵了很多促進特殊代碼測試的擴展的誕生。這些擴展的目標是對數據庫代碼、EJB,Servlets等進行測試。JUnit也能很好的和一些開發環境(工具)相結合。JUnit Ant Task允許Ant調用JUnit的測試任務(要了解更多關於Ant,見Nicholas Serrano 和 Ismael Ciordia 2004年11、12月份的專欄)。JUnit的官方網站上包括了很多JUnit的擴展和與之結合的開發環境的介紹和鏈接。
    如果你最喜歡開發語言不是Java,其他的語言也提供了JUnit類似的接口和框架,xUNIT的符號代表了整個家族:CppUnit負責C++的單元測試;perlUnit是Perl的單元測試接口。PyUnit是Python語言的單元測試標準框架;Test::Unit則控制了整個Ruby社區;Nunit是用C#完成的,爲.Net平臺提供了開源的單元測試支持。
    TestNG(www.beust.com/testng)是近來興起的一個JUnit的替代品,吸引了很多開發者的注意,ParaSoft的Jtest(www.parasoft.com/jtest)是一個流行並且經濟的測試工具。在表格中把上述工具和JUnit進行了比較。
    在表格中,我們可以看到Jtest提供了額外的一些特性,而這些特性你可能會認爲只有那些比開源軟件大100倍的商業軟件才能擁有。不管怎樣,這寫額外的特性反映了一些不同的理念。本質上,JUnit是一個很簡單的類庫。事實上,如果你願意,你可以從頭開始寫。JUnit在設計的時候,被設計爲只用來做一件事情,但是一定要做好它。這個理念和Unix的工具的理念非常類似:讓開發人員可以放入工具箱中,從而得到額外的功能。幾個常見的JUnit例子有:JTestCase (http://jtestcase.sourceforge.net)
 JUnitDoclet(www.JUnitdoclet.org)
 JUnitPerf (www.clarkware.com/software/JUnitPerf.html)
 MockObjects(www.mockobjects.com)
 EasyMock (www.easymock.org)。
而另一方面,商業的測試框架則提供了儘可能多的功能和綜合環境。
五、測試覆蓋
    在編碼和測試的過程中,你必須確認所有的代碼都被測試到了。JUnit能幫助編寫測試,然而選擇一個完整的測試集卻依賴於每一個程序員。在測試中有很多種測試覆蓋。例如語句覆蓋(也被稱爲線性覆蓋),結果覆蓋(也被稱爲分支覆蓋或路徑覆蓋)等等。如果你正在爲一個開源項目工作,就能得到Clover(http://www.cenqua.com/clover/)這是一個商業的測試覆蓋工具,但遵循了非商業的許可證。另外一個商業的測試覆蓋工具Jcoverage (www.jcoverage.com)擁有一個遵循GPL的版本,但在功能上有所限制。GroboUtils (http://groboutils.sourceforge.net)包含了一個覆蓋分析的包。Emma (http://emma.sourceforge.net)是另外一個易用的工具。兩個JUnit特有的工具包是NoUnit (http://nounit.sourceforge.net/using.html) 和 Jester (http://jester.sourceforge.net)。GNU的編譯器網站也包含了一個叫GCov的工具。
六、開發過程種的單元測試
    援引Glenford Myers關於測試的經典書中的一句話:不要丟棄測試用例,除非程序被丟棄("Avoid throwaway test cases unless the program is truly a throwaway program."見JUnit Resources欄)。你必須讓你的代碼和測試保持同步,做到這一點,無論什麼時候代碼改變了,迴歸測試都很簡單。
    用心計劃的測試集合能創建非常有效的項目文檔。看一個類如何運行的最好辦法就是看它的運行,而單元測試用例包含的代碼片斷則正好能滿足這一點。
    一個綜合的測試集合並不會浪費運行資源,因爲測試代碼不會干擾程序。但是測試會消耗一定的編譯時的資源,不斷增加的測試用例能拖慢編譯的過程,要避免這個狀況,開發人員可以按需要定製編譯過程來完成系統的構建。比如可以用Ant來定義一組測試任務,這些任務能在每天結束的時候,在代碼提交以前執行。
當然,單元測試只是測試環節中的很小一部分。除單元測試以外,我們還有將某一功能相關的代碼一起執行測試,這就是功能測試,功能測試下面以後,就是整個系統的測試(系統測試);在交付以前,最終用戶會看軟件是否達到了預定的需求(接受測試)等等。但是爲軟件的最小部分測試添加很多的舒適和樂趣會給後面的測試打下結實的基礎。
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章