JUnit使用經驗

iteye_9089發表於2010-08-29
很多測試人員都有過編寫測試框架的經歷,JUnit的出現避免了其中的大量重複勞動。但如同其他的工具一樣用得好和用得差的結果是截然不同的。我們編輯這樣一個JUnit的系列希望能夠幫助越來越多的JUnit使用者用好JUnit。我們也希望讀者們能夠把自己的一些經驗所得和大家分享。

經驗一、不要在測試用例的建構函式中做初始化
答案是過載測試用例的setUp()方法進行初始化。

經驗二、不要假定測試用例中測試的執行次序
好的習慣是保持測試之間的獨立性,使得它們在任何次序下執行的結果都是相同的。

經驗三、測試要避免人工干預
經驗二講的是不同的測試要避免相關性,而經驗三講的其實就是測試要避免自相關。

經驗四、在子類中呼叫父類的setUp() 和tearDown()

經驗五、不要硬性規定資料檔案的路徑

經驗六、把測試的程式碼和被測的程式碼放在同樣的目錄下

經驗七、正確命名測試

經驗八、書寫測試時要考慮地區和國家設定

經驗九、利用Junit 的自動異常處理書寫簡潔的測試程式碼
事實上在Junit 中使用try-catch 來捕獲異常是沒有必要的,Junit 會自動捕獲異常。那些沒有被捕獲的異常就被當成錯誤處理。

經驗十、充分利用Junit 的assert/fail 方法
assertSame()用來測試兩個引用是否指向同一個物件
assertEquals()用來測試兩個物件是否相等

經驗十一、確保測試程式碼與時間無關

經驗十二、使用文件生成器做測試文件



經驗一、不要在測試用例的建構函式中做初始化
當我們需要增加一個測試時,我們要書寫一個自己的測試用例,比如SomeTest。如果你喜歡在SomeTest的
建構函式中做有關的初始化工作,這可不是個好習慣。如下例:


public Class SomeTestclass SomeTest extends TestCase{
public SomeTest(String testName){
super(testName);
//初始化程式碼
}
}
一旦初始化程式碼產生異常,比如IllegalStateException,JUnit隨之將產生一個AssertionFailedError,
並顯示類似下面的出錯資訊:
junit.framework.AssertionFailedError:Cannotinstantiatetestcase:test1at
junit.framework.Assert.fail(Assert.java:143at
junit.framework.TestSuite$1.runTest(TestSuite.java:178at
junit.framework.TestCase.runBare(TestCase.java:129at
junit.framework.TestResult$1.protect(TestResult.java:100at
junit.framework.TestResult.runProtected(TestResult.java:117at
junit.framework.TestResult.run(TestResult.java:103at
junit.framework.TestCase.run(TestCase.java:120at
junit.framework.TestSuite.run(TestSuite.java,CompiledCodeat
junit.ui.TestRunner$12.run(TestRunner.java:429
這一大堆出錯資訊只會讓人一頭霧水,我們只知道JUnit無法例項化某個測試用例,到底出了什麼問題,在
哪兒出錯了呢?不知道!
那麼好的做法是怎樣呢?
答案是過載測試用例的setUp()方法進行初始化。當setUp()中的初始化程式碼產生異常時我們得到的
是類似下面的出錯資訊:
java.lang.IllegalStateException:Oopsatbp.DTC.setUp(DTC.java:34at
junit.framework.TestCase.runBare(TestCase.java:127at
junit.framework.TestResult$1.protect(TestResult.java:100at
junit.framework.TestResult.runProtected(TestResult.java:117at
junit.framework.TestResult.run(TestResult.java:103
...
顯然這要清楚得多我們一下子就可以知道是在DTC.java 的第34 行產生了IllegalStateException



經驗二、不要假定測試用例中測試的執行次序
我們知道在一個JUnit 的測試用例類中可以包含多個測試,每個測試其實就是一個method。在下面的例子
中有兩個不同的測試,儘管testDoThisFirst()在位置上先於testDoThisSecond(),但我們不能就此假定
testDoThisFirst()會先執行。


public Class SomeTestCaseclass SomeTestCase extends TestCase{
public SomeTestCase(String testName){
super(testName);
}
public void testDoThisFirst(){

}
public void testDoThisSecond(){
}
}由於JUnit 內部使用一個Vector 來儲存所有的test,因此在不同的作業系統和Java 虛擬機器上,test 的執行
次序是不可預測的。
好的習慣是保持測試之間的獨立性,使得它們在任何次序下執行的結果都是相同的。如果真得需要某些測試
按照特定的次序執行,我們可以藉助addTest 來實現。如下例:


public static Testsuite(){
suite.addTest(new SomeTestCase(“testDoThisFirst”);
suite.addTest(new SomeTestCase(“testDoThisSecond”);
return suite;
}
這樣我們可以確保JUnit先執行testDoThisFirst(),然後執行testDoThisSecond()。

經驗三、測試要避免人工干預
如果某段測試程式碼需要人工干預,那至少有兩個不良後果:一則不能被包括在自動測試中,比如夜間的回
歸測試;二則不能被重複執行,例如資料刪除的測試不能做完刪除就萬事大吉,比較好的做法是自動補上
刪除掉的資料。經驗二講的是不同的測試要避免相關性,而經驗三講的其實就是測試要避免自相關。

經驗四、在子類中呼叫父類的setUp() 和tearDown()讓我們看一看下面的程式碼

public Class SomeTestCaseclass SomeTestCase extends AnotherTestCase {
// A connection to a database
private Database theDatabase;
public SomeTestCase (String testName) {
super (testName);
}
public void testFeatureX () {

}
public void setUp () {
// Clear out the database
theDatabase.clear ();
}
}你發現其中的錯誤了嗎?setUp()應該呼叫super.setUp() 以確保AnotherTestCase 中定義的環境被初
始化了。當然這也有例外,就是基類可以處理任意的測試資料。


經驗五、不要硬性規定資料檔案的路徑
我們經常需要從檔案系統中讀取測試資料,看下面的程式碼:

public void setUp () {
FileInputStream inp ("C:\TestData\dataSet1.dat";

}
這段程式碼需要把測試資料檔案dataSet1.dat 放在C:TestData,這是有問題的。
第一,C 盤可能沒有磁碟空間了測試人員不得不把資料檔案放到其他路徑;
第二,可能需要在其他作業系統比如Linux 上執行這一測試。
所以,一個較好的替代方案是

public void setUp () {
FileInputStream inp ("dataSet1.dat";

}

但事實上這樣仍不是很好,因為這要求資料檔案的路徑和測試執行的路徑必須是同一個,如果幾個不同
的測試都這樣的話,那要把這些測試集合起來執行就有些困難,我們不得不頻繁的改變當前路徑。
為了解決這個問題,我們可以使用Class.getResource()或者Class.getResourceAsStream(),這樣我
們可以把資料檔案放在這個Class 的某個相對路徑上。
資料檔案應該儘可能和原始碼一起都放在配置管理系統上,但這樣一來如果我們採用上面的Resource 機
制,我們就需要做一件工作,就是把資料檔案從原來的位置-就是原始碼的某個相對路徑,拷貝到編譯後
的位置,也就是class 檔案的相應的相對路徑。這其實並不複雜,因為從class 的package 就可以對映到
java檔案的所在路徑對於Linux或者Windows我們所要做的就是把package中的. 用
File.separatorChar 替代。



經驗六、把測試的程式碼和被測的程式碼放在同樣的目錄下
當我們把測試程式碼和被測的程式碼放在同一目錄下時,我們就可以在編譯被測程式碼的同時編譯測試程式碼,從
而確保兩者是同步更新的。事實上當前的普遍做法,就是把單元測試視為Build 的一個環節。

經驗七、正確命名測試
把測試用例命名為TestClassUnderTest,比如如果被測的Class 是MessageLog,那麼測試用例就叫
TestMessageLog,這樣做使得測試用例和被測的Class一一對應,而在測試用例中每個測試的method 就
可以命名為
testLoggingEmptyMessage()
testLoggingNullMessage()
testLoggingWarningMessage()
testLoggingErrorMessage()
同樣是為了說清楚測試的是什麼。正確的命名可以幫助測試程式碼的閱讀者瞭解每個測試的目的。



經驗八、書寫測試時要考慮地區和國家設定
比如某個測試要使用日期,下面的程式碼就是建立日期物件的一種方法
Date date = DateFormat.getInstance ().parse ("dd/mm/yyyy";
但是如果執行上面測試程式碼的機器採用不同的地區國家設定,那麼就會有問題。因此我們最好用下面的另
一種方法:
Calendar cal = Calendar.getInstance ();
Cal.set (yyyy, mm-1, dd);
Date date = Calendar.getTime ();
顯然,第二種方法能夠適應地區國家設定的變化。



經驗九、利用Junit 的自動異常處理書寫簡潔的測試程式碼
很多Junit 的初學者經常會寫出類似下面的這些程式碼


public void exampleTest () {
try {
// do some test
} catch (SomeApplicationException e) {
fail ("Caught SomeApplicationException exception";
}
}
事實上在Junit 中使用try-catch 來捕獲異常是沒有必要的,Junit 會自動捕獲異常。那些沒有被捕獲的異常就
被當成錯誤處理。所以上面的程式碼很冗餘,完全可以寫成下面等效卻簡潔得多的程式碼:

public void exampleTest () throws SomeApplicationException {
// do some test
}
更少的測試程式碼也更容易讀懂更容易維護。




經驗十、充分利用Junit 的assert/fail 方法
Junit 有豐富而靈活的assert/fail 方法,如何用好這些方法也是大有講究的。比如下面的寫法就不大好
assert (creds == 3);
不如寫成
assertEquals ("The number of credentials should be 3", 3, creds);
第二種寫法不僅易於閱讀,而且在執行時如果fail 也可以向測試人員提供更多的資訊。
Junit 也有支援浮點數的assert方法,乾淨利索如下例
assertEquals ("some message", result, expected, delta);
另外要一提的是:
assertSame()用來測試兩個引用是否指向同一個物件
assertEquals()用來測試兩個物件是否相等


經驗十一、確保測試程式碼與時間無關
儘量避免可能過期的測試資料,這種資料應該可以手工或者自動的重新整理。另外還有一個技巧就是在使用這些
資料前更改系統的當前日期,資料操作結束後再恢復日期。當然,使用這一技巧要注意可能的副作用。

經驗十二、使用文件生成器做測試文件
我們當然可以使用文字編輯器來書寫單元測試的文件,但是更好的方法是使用文件生成器比如JavaDoc自
動生成,這樣我們就不需擔心實現和文件之間的同步問題。自動生成的文件格式統一錯誤也少。

相關文章