模仿對象(Mock object)是為起中介者作用的對象編寫單元測試的有用方法。測試對象調(diào)用模仿域?qū)ο螅ㄋ粩嘌砸哉_的次序用期望的參數(shù)調(diào)用了正確的方法),而不是調(diào)用實際域?qū)ο蟆H欢,?dāng)測試對象必須創(chuàng)建域?qū)ο髸r,我們面臨一個問題。測試對象如何知道創(chuàng)建模仿域?qū)ο,而不是?chuàng)建實際域?qū)ο竽?在本文中,軟件顧?Alexander Day Chaffee 和 William Pietri 將演示一種重構(gòu)技術(shù),該技術(shù)根據(jù)工廠方法設(shè)計模式來創(chuàng)建模仿對象。
單元測試已作為軟件開發(fā)的“佳實踐”被普遍接受。當(dāng)編寫對象時,還必須提供一個自動化測試類,該類包含測試該對象性能的方法、用各種參數(shù)調(diào)用其各種公用(public)方法并確保返回值是正確的。
當(dāng)您正在處理簡單數(shù)據(jù)或服務(wù)對象時,編寫單元測試很簡單。然而,許多對象依賴基礎(chǔ)結(jié)構(gòu)的其它對象或?qū)。?dāng)開始測試這些對象時,實例化這些合作者(collaborator)通常是昂貴的、不切實際的或效率低的。
例如,要單元測試一個使用數(shù)據(jù)庫的對象,安裝、配置和發(fā)送本地數(shù)據(jù)庫副本、運行測試然后再卸裝本地數(shù)據(jù)庫可能很麻煩。模仿對象提供了解決這一困難的方法。模仿對象符合實際對象的接口,但只要有足夠的代碼來“欺騙”測試對象并跟蹤其行為。例如,雖然某一特定單元測試的數(shù)據(jù)庫連接始終返回相同的硬連接結(jié)果,但可能會記錄查詢。只要正在被測試的類的行為如所期望的那樣,它將不會注意到差異,而單元測試會檢查是否發(fā)出了正確的查詢。
夾在中間的模仿
使用模仿對象進行測試的常用編碼樣式是:
· 創(chuàng)建模仿對象的實例
· 設(shè)置模仿對象中的狀態(tài)和期望值
· 將模仿對象作為參數(shù)來調(diào)用域代碼
· 驗證模仿對象中的一致性
雖然這種模式對于許多情況都非常有效,但模仿對象有時不能被傳遞到正在測試的對象。而設(shè)計該對象是為了創(chuàng)建、查找或獲得其合作者。
例如,測試對象可能需要獲得對Enterprise JavaBean(EJB)組件或遠程對象的引用。或者,測試對象會使用具有副作用的對象,如刪除文件的File對象,而在單元測試中不希望有這些副作用。
根據(jù)常識,我們知道這種情形下可以嘗試重構(gòu)對象,使之更便于測試。例如,可以更改方法簽名,以便傳入合作者對象。
在 Nicholas Lesiecki 的文章“Test flexibly with AspectJ and mock objects”中,他指出重構(gòu)不一定總是合意的,也不一定總是產(chǎn)生更清晰或更容易理解的代碼。在許多情況下,更改方法簽名以使合作者成為參數(shù)將會在方法的原始調(diào)用者內(nèi)部產(chǎn)生混淆的、未經(jīng)試驗的代碼混亂。
問題的關(guān)鍵是該對象“在里面”獲得這些對象。任何解決方案都必須應(yīng)用于這個創(chuàng)建代碼的所有出現(xiàn)。為了解決這個問題,Lesiecki 使用了查找方式或創(chuàng)建方式。在這個解決方案中,執(zhí)行查找的代碼被返回模仿對象的代碼自動替換。
因為 AspectJ 對于某些情況不是選項,所以我們在本文中提供了一個替代方法。因為在根本上這是重構(gòu),所以我們將遵循 Martin Fowler 在他創(chuàng)新的書籍“Refactoring: Improving the Design of Existing Code”(請參閱參考資料)中建立的表達約定。(我們的代碼基于 JUnit — Java 編程的流行的單元測試框架,盡管它決不是 JUnit 特定的。)
重構(gòu):抽取和覆蓋工廠方法
重構(gòu)是一種代碼更改,它使原始功能保持不變,但更改代碼設(shè)計,使它變得更清晰、更有效且更易于測試。本節(jié)將循序漸進地描述“抽取”和“覆蓋”工廠方法重構(gòu)。
問題:正在測試的對象創(chuàng)建了合作者對象。必須用模仿對象替換這個合作者。
重構(gòu)之前的代碼:
class Application {
...
public void run() {
View v = new View();
v.display();
...
解決方案:將創(chuàng)建代碼抽取到工廠方法,在測試子類中覆蓋該工廠方法,然后使被覆蓋的方法返回模仿對象。后,如果可以的話,添加需要原始對象的工廠方法的單元測試,以返回正確類型的對象:
重構(gòu)之后的代碼:
class Application {
...
public void run() {
View v = createView();
v.display();
...
protected View createView() {
return new View();
}
...
}
該重構(gòu)啟用清單1中所示的單元測試代碼:
清單 1. 單元測試代碼
class ApplicationTest extends TestCase {
MockView mockView = new MockView();
public void testApplication {
Application a = new Application() {
protected View createView() {
return mockView;
}
};
a.run();
mockView.validate();
}
private class MockView extends View
{
boolean isDisplayed = false;
public void display() {
isDisplayed = true;
}
public void validate() {
assertTrue(isDisplayed);
}
}
}