怎樣最有效地測試異常?

工作中,和同事對測試異常的最佳方法產生了分歧。

我是比較欣賞JUnit4的@Test(expected=FooException.class)的啦,覺得這樣多清爽啊,多declarative啊,再不用寫那麼一大坨try-fail-catch了。

不過同事(以下簡稱S)不這麼認爲。他覺得try-fail-catch挺好的,價格便宜,量又足,我們一直用它。而JUnit 4和TestNG提供的這個功能容易引誘程序員犯錯誤。

S給提出了一個挑戰:

[code]
public void testDoSomethingBad() {
initializeSomething();
try {
doSomethingBad();
fail();
} catch (FooException e) {}
}
[/code]

這裏面initializeSomething()的作用是初始化到某一個狀態,這個過程不應該出錯,而到了這個狀態之後,doSomethingBad()纔會拋異常。

然後他堅持認爲這種情況是最普遍的情況。而用annotation雖然看上去很美,但是可能邪惡地誘惑程序員寫出不準確的測試,造成false positive,比如,initializeSomething()拋了一個異常。

當然,我們對這種情況的常見程度各執己見。也沒什麼說的。但是,後來我想,其實,這個測試換成自然語言表達是什麼呢?大概是這樣吧?
[list]
[*] initializeSomething()不許出錯
[*] 在initializeSomething()之後doSomethingBad()要出錯
[/list]

那麼,爲什麼不把這兩個要求寫成兩個測試呢?
[code]
@Test
public void testInitializeSomething() {
initializeSomething();
}

@Test(expected=FooException.class)
public void testDoSomethingBadAfterInitializeSomething() {
initializeSomething();
doSomethingBad();
}
[/code]

只要我們寫測試的時候不要總想着“聰明”地實現,而是直白地用代碼表示需求,不就沒問題了麼?

再說一說我爲什麼這麼討厭這個try-fail-catch。它有幾個我深惡痛絕的毛病。
[list]
[*] 它等於代碼裏的邏輯分支。如果沒有拋異常,它執行fail(),而如果拋了異常,它進入catch()。而測試裏的邏輯分支味道很壞。它讓你的代碼容易出錯(比如,你忘了fail怎麼辦?測試一樣是綠的,但是你的bug還躲在那)。而且,它讓測試代碼不能達到100%的分支覆蓋率。本來如果用annotation的話,如果出現了initializeSomething()拋出異常的情況,覆蓋率馬上不是100%了,你可以很容易地發現問題。
[*] 冗長煩瑣。測試寫的不象spec,而象過程形代碼。
[*] 這個try-fail-catch只在你檢查Exception的時候成立。如果萬一你要檢查一個Error甚至是JUnit的AssertionFailedError,完了,你連fail()拋的異常也給截獲了。
[/list]

今天早晨,忽然靈機一動,其實,還有一個方法的。比如,在你自己的BaseTest的基類裏面,你可以實現一個expectException()的函數,然後這麼用:
[code]
public void testDoSomethingBad() {
initializeSomething();
expectException(FooException.class);
doSomethingBad();
}
[/code]

這樣,在runTest()結束前,可以檢查是否存在一個exception expectation,如果有,就catch住拋出來的異常,然後進行檢查。而如果沒出現異常,直接就報錯。這樣,不就沒問題了?還可以進一步抽象,弄個ExceptionExpectation的接口,這樣客戶代碼可以靈活地登記並且重用任何的異常期待,不僅僅侷限於檢查異常類型和錯誤信息了。

當然,這是在JUnit 3.8。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章