JUnit Sucks

好象貌似有本書是用寫一個JUnit作爲例子來講解TDD的。要說TDD絕對是個好東西,不過TDD本身並不能保證搞出好軟件。這不,Junit就是個活生生的例子呀。

一直以來,我寫Junit+Easymock測試都是這麼來的:
[code]
public class SomeTest extends TestCase {
private final IMocksControl control = EasyMock.createStrictControl();
private final Connection connection = control.createMock(Connection.class);
@Override protected void tearDown() {
control.verify();
// other cleanups.
}
public void test1() {
expect(connection.createStatement()).andReturn(null);
...
control.replay();
...
}
}
[/code]

不要細摳具體的mock用法,我這都是臨時編的。但是這個測試類有兩個肥腸嚴重的問題。說起來我一直都是這麼做的,真是愧對付給我工資的那些老闆們啊!

第一個問題:

JUnit裏面不應該用field initializer的——或者說,不能用構造函數來初始化test fixture的。當調用new TestSuite(SomeTest.class)的時候,JUnit系統會對每一個testSomething()函數調用一次構造函數,生成一個獨立的test instance. 這樣做,保證了每個測試的獨立性,最大程度地避免了某個test case影響其它test case的出現。

不過,“等等”,你說。這不是正好就對了麼?每個test case都會調用一遍這些field initializer,沒有任何問題了呀?

是呀,所以我才這麼多年一直理直氣壯,厚顏無恥地用這個idiom呀,用這個idiom。直到最近,同事中的java專家告訴我,這是不安全地,不道德地,是低級趣味地!
因爲JUnit在沒有執行test case,只是構造test suite的時候,就調用了這些構造函數,這就造成了幾個額外問題:
1。這造成TestSuite的創建不夠安全,有可能出現異常——我們希望異常只出現在test case運行的時候。並且TestSuite的創建速度也有可能受到影響。
2。我們無法保證一個TestCase只被運行一次。比如說RepeatedTest就會多次運行一個TestCase。所以保證TestCase至少看上去是immutable的就比較重要了。

第二個問題:
不應該在tearDown()裏面調用verify()。如果test case失敗了,首先這個verify()就沒有調用的必要了。不應該用tearDown()來強制調用它。其次,如果verify()也失敗(相當有可能地),那麼後一個exception會沖掉前面的那個(JUnit 4裏面修好了這個bug),而前面那個exception纔是你真正需要的呀。

而且,如果verify()失敗了,那麼後面的other cleanups也被跳過去了,而它們纔是最最重要的呀。

你說可氣不可氣?我還以爲JUnit這麼簡單的東西,肯定不會用錯的呢。

但是,讓我不許定義final field,讓我把所有的初始化都放在setUp裏面,讓我在tearDown裏面一個接一個地套try-finally,這,這不是代碼難看的問題,這是太沒人性了!

我想啊想,終於頓悟了。這不是人民內部矛盾,而是不可調和的階級衝突啊。


JUnit不同於TestNG, 它極度強調測試的隔離性,爲此不惜禁止我們在一個測試類中共享一些有用的信息(比如,一個parse好的xml數據之類),每個test method都將使用一個單獨的instance。可是,如此代價換來的居然還是一個不幹不嘎的局面:我們還是不能假設一個instance只被用一次。它肯定不會被兩個不同的test method使用地,——但是它可以被一個test method重複使用啊。哈,哈,哈。沒想到吧?

到了JUnit 4, tearDown的問題解決了。我可以在每個@After函數中釋放各自的資源,框架會保證它們都被執行。不過,我們還是不能在@After的函數中調用verify(), 還是因爲對於verify()我們並不希望在測試本身失敗的時候還調用它。

至於構造函數問題,沒有任何改善,沒有。同樣,JUnit 4也沒有正視廣大人民羣衆對共享數據的呼聲。不管TestSetup還是@BeforeClass都是要求你用evil static field。

忍無可忍之下,我又怒了。於是自己寫了一個AjooTestSuite,偷偷從TestNG搞來了幾個我一直非常眼饞的annotation: @BeforeTest, @AfterTest, @BeforeSuite, @AfterSuite, @ExceptionExpected。又自己填了兩個@Verify和@Shared。前者用來在測試沒出問題的情況下做一個公用的verify,比如verify mock object;後者用來在test case之間共享數據。一個使用AjooTestSuite的測試類可以這麼寫:

[code]
public class SomeTest extends TestCase {
public static Test suite() {
return new AjooTestSuite(SomeTest.class);
}

// 哈哈。終於可以用final和initializer了!
private final IMocksControl control = EasyMock.createStrictControl();
private final Connection conn = control.createMock(Connection.class);

@Verify
public void verifyMocks() {
control.verify();
}

@Shared
public static XmlObject provideXml() {
// ... read xml file and parse.
return xmlObj;
}

private final XmlObj;

// the shared xmlObj will be injected for each test case.
public SomeTest(String name, XmlObject obj) {
super(name);
this.xmlOj = obj;
}

@BeforeSuite
public static void initialize() {
// some suite level initialization. No more TestSetup!
}
@AfterSuite
public static void deinitialize() {
// suite level deinitialization.
}
@BeforeTest
public void setupMocks() {
expect(conn.createStatement()).andReturn(null);
}
@ExceptionExpected(NullPointerException.class)
public void test1() {
throw new NullPointerException();
}
}
[/code]


寫完之後,我這個得意呀。終於不用忍受JUnit屎一樣的限制了。構造函數只有在測試運行的時候才調用,換句話說,這回test case是絕對immutable的了,絕對線程安全。耶!

除此之外,還加上了一些流行的annotation。@BeforeTest, @AfterTest, @BeforeSuite, @AfterSuite,@ExceptionExpected不用說,都跟TestNG一樣的語義。@Verify用來搞類似verify mock之類的事情;@Shared標註的靜態函數在每次test suite執行的時候會被調用一次,返回的數據會被保存並被注射給每一個test case instance。

完美呀,完美。這樣一來,也不用轉移到TestNG了,也不用忍受一些工具集成的問題升級到JUnit4了。就是一個新的TestSuite而已,100%向後兼容。

可是,事實證明,我說的太早了。我的java專家同事又給我潑了一瓢涼水:

現在,你在Eclipse/Intellij裏面點運行,沒問題,它會知道去尋找suite()函數,然後用你的自定義test suite。可是,如果你然後點某一個單獨的test case, 比如test1, 然後說"run"。會發生什麼哩?嘿嘿,它不再去找你的suite()函數啦,啦,啦,啦,啦(回聲逐漸消失)。它直接跑到你的類裏面去找這個函數來調用了。Surprise!

天啊。該死的Eclipse, 它難道不會調用suite(), 然後在suite()裏面找麼?

可仔細想想,又不是IDE的錯。即使它調用suite()又如何?沒有一個TestSuite.getTest(String name)的API供它調用啊。實際上,JUnit的TestCase也並不強制保證每個Test都有一個id的。

於是,我三天的工作白費了。嗚嗚嗚!

總而言之,言而總之,千言萬語,咬牙切齒匯成一句話:JUnit Sucks!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章