国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

使用模仿對象進行單元測試

2019-11-18 15:15:37
字體:
來源:轉載
供稿:網友

  用模擬對象替換合作者以改進單元測試
級別:中級

Alexander Day Chaffee(alex@jguru.com),顧問,Purple Technology
William Pietri(william@scissor.com),顧問,Scissor

模擬對象(Mock object)是為起中介者作用的對象編寫單元測試的有用方法。測試對象調用模擬域對象(它只斷言以正確的次序用期望的參數調用了正確的方法),而不是調用實際域對象。然而,當測試對象必須創建域對象時,我們面臨一個問題。測試對象如何知道創建模擬域對象,而不是創建實際域對象呢?在本文中,軟件顧問 Alexander Day Chaffee 和 William Pietri 將演示一種重構技術,該技術根據工廠方法設計模式來創建模擬對象。
單元測試已作為軟件開發的“最佳實踐”被普遍接受。當編寫對象時,還必須提供一個自動化測試類,該類包含測試該對象性能的方法、用各種參數調用其各種公用(public)方法并確保返回值是正確的。

當您正在處理簡單數據或服務對象時,編寫單元測試很簡單。然而,許多對象依靠基礎結構的其它對象或層。當開始測試這些對象時,實例化這些合作者(collaborator)通常是昂貴的、不切實際的或效率低的。

例如,要單元測試一個使用數據庫的對象,安裝、配置和發送本地數據庫副本、運行測試然后再卸裝本地數據庫可能很麻煩。模擬對象提供了解決這一困難的方法。模擬對象符合實際對象的接口,但只要有足夠的代碼來“欺騙”測試對象并跟蹤其行為。例如,雖然某一特定單元測試的數據庫連接始終返回相同的硬連接結果,但可能會記錄查詢。只要正在被測試的類的行為如所期望的那樣,它將不會注重到差異,而單元測試會檢查是否發出了正確的查詢。

夾在中間的模擬
使用模擬對象進行測試的常用編碼樣式是:

創建模擬對象的實例
設置模擬對象中的狀態和期望值
將模擬對象作為參數來調用域代碼
驗證模擬對象中的一致性
雖然這種模式對于許多情況都非常有效,但模擬對象有時不能被傳遞到正在測試的對象。而設計該對象是為了創建、查找或獲得其合作者。

例如,測試對象可能需要獲得對 EnterPRise javaBean(EJB)組件或遠程對象的引用。或者,測試對象會使用具有副作用的對象,如刪除文件的 File 對象,而在單元測試中不希望有這些副作用。

根據常識,我們知道這種情形下可以嘗試重構對象,使之更便于測試。例如,可以更改方法簽名,以便傳入合作者對象。

在 Nicholas Lesiecki 的文章“Test flexibly with aspectJ and mock objects”中,他指出重構不一定總是合意的,也不一定總是產生更清楚或更輕易理解的代碼。在許多情況下,更改方法簽名以使合作者成為參數將會在方法的原始調用者內部產生混淆的、未經試驗的代碼混亂。

問題的要害是該對象“在里面”獲得這些對象。任何解決方案都必須應用于這個創建代碼的所有出現。為了解決這個問題,Lesiecki 使用了查找方式或創建方式。在這個解決方案中,執行查找的代碼被返回模擬對象的代碼自動替換。

因為 AspectJ 對于某些情況不是選項,所以我們在本文中提供了一個替代方法。因為在根本上這是重構,所以我們將遵循 Martin Fowler 在他創新的書籍“Refactoring: Improving the Design of Existing Code”(請參閱參考資料)中建立的表達約定。(我們的代碼基于 JUnit ? Java 編程的最流行的單元測試框架,盡管它決不是 JUnit 特定的。)

重構:抽取和覆蓋工廠方法
重構是一種代碼更改,它使原始功能保持不變,但更改代碼設計,使它變得更清楚、更有效且更易于測試。本節將循序漸進地描述“抽取”和“覆蓋”工廠方法重構。

問題:正在測試的對象創建了合作者對象。必須用模擬對象替換這個合作者。

重構之前的代碼 class application {
...
public void run() {
View v = new View();
v.display();
...

解決方案:將創建代碼抽取到工廠方法,在測試子類中覆蓋該工廠方法,然后使被覆蓋的方法返回模擬對象。最后,假如可以的話,添加需要原始對象的工廠方法的單元測試,以返回正確類型的對象:

重構之后的代碼 class Application {
...
public void run() {
View v = createView();
v.display();
...
protected View createView() {
return new View();
}
...
}

該重構啟用清單 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);
}
}
}

角色
該設計引入了由系統中的對象扮演的下列角色:

目標對象:正在測試的對象
合作者對象:由目標對象創建或獲取的對象
模擬對象:遵循模擬對象模式的合作者的子類(或實現)
非凡化對象:覆蓋創建方法以返回模擬對象而不是合作者的目標的子類
技巧
重構由許多小的技術性步驟組成。這些步驟統稱為技巧。假如您象按照食譜那樣嚴格遵循這些技術,那么您在學習重構時應該沒有太大的麻煩。

標識創建或獲取合作者的代碼的所有出現。

將抽取方法重構應用于這個創建代碼,創建工廠方法(在 Fowler 書籍的第 110 頁中討論;有關更多信息,請參閱參考資料一節)。

確保目標對象及其子類可以訪問工廠方法。(在 Java 語言中,使用 protected 要害字)。

在測試代碼中,創建模擬對象且實現與合作者相同的接口。

在測試代碼中,創建擴展(專用于)目標對象的非凡化對象。

在非凡化對象中,覆蓋創建方法以返回為測試提供的模擬對象。

可選的:創建單元測試以確保原始目標對象的工廠方法仍返回正確的非模擬對象。
示例:ATM
設想您正在編寫用于銀行自動柜員機(Automatic Teller Machine)的測試。其中一個測試可能類似于清單 2:

清單 2. 初始單元測試,在模擬對象引入之前 public void testCheckingWithdrawal() {
float startingBalance = balanceForTestCheckingAccount();

AtmGui atm = new AtmGui();
insertCardAndInputPin(atm);

atm.pressButton("Withdraw");
atm.pressButton("Checking");
atm.pressButtons("1", "0", "0", "0", "0");
assertContains("$100.00", atm.getDisplayContents());
atm.pressButton("Continue");

assertEquals(startingBalance - 100,
balanceForTestCheckingAccount());
}

另外,AtmGui 類內部的匹配代碼可能類似于清單 3:

清單 3. 產品代碼,在重構之前 private Status doWithdrawal(Account account, float amount) {
Transaction transaction = new Transaction();
transaction.setSourceAccount(account);
transaction.setDestAccount(myCashAccount());
transaction.setAmount(amount);
transaction.process();
if (transaction.sUCcessful()) {
dispense(amount);
}
return transaction.getStatus();
}

該方法將起作用,遺憾的是,它有一個副作用:支票帳戶余額比測試開始時少,這使得其它測試變得更困難。有一些解決這種困難的方法,但它們都會增加測試的復雜性。更糟的是,該方法還需要對治理貨幣的系統進行三次往返。

要修正這個問題,第一步是重構 AtmGui 以答應我們用模擬事務替換實際事務,如清單 4 中所示(比較粗體的源代碼以查看我們正在更改什么):

清單 4. 重構 AtmGui private Status doWithdrawal(Account account, float amount) {
Transaction transaction = createTransaction();
transaction.setSourceAccount(account);
transaction.setDestAccount(myCashAccount());
transaction.setAmount(amount);
transaction.process();
if (transaction.successful()) {
dispense(amount);
}
return transaction.getStatus();
}
protected Transaction createTransaction() {
return new Transaction();
}

后退到測試類內部,我們將 MockTransaction 類定義為成員類,如清單 5 中所示:

清單 5. 將 MockTransaction 定義為成員類 private MockTransaction extends Transaction {

private boolean processCalled = false;

// override process method so that no real work is done
public void process() {
processCalled = true;
setStatus(Status.SUCCESS);
}

public void validate() {
assertTrue(processCalled);
}
}

最后,我們可以重寫測試,以便被測試的對象使用 MockTransaction 類,而不是使用實際類,如清單 6 中所示:

清單 6. 使用 MockTransaction 類 MockTransaction mockTransaction;

public void testCheckingWithdrawal() {
mockTransaction = new MockTransaction();

AtmGui atm = new AtmGui() {
protected Transaction createTransaction() {
return mockTransaction;
}
};

insertCardAndInputPin(atm);

atm.pressButton("Withdraw");
atm.pressButton("Checking");
atm.pressButtons("1", "0", "0", "0", "0");
assertContains("$100.00", atm.getDisplayContents());
atm.pressButton("Continue");
assertEquals(100.00, mockTransaction.getAmount());
assertEquals(TEST_CHECKING_ACCOUNT,
mockTransaction.getSourceAccount());
assertEquals(TEST_CASH_ACCOUNT,
mockTransaction.getDestAccount());
mockTransaction.validate();
}

該解決方案產生了一個稍長的測試,但該測試只關注正在測試的類的直接行為,而不是 ATM 接口之外整個系統的行為。也就是說,我們不再檢查測試帳戶的最終余額是否正確;我們將在對 Transaction 對象的單元測試中檢查該函數,而不是在對 AtmGui 對象的單元測試中。

注:根據模擬對象的創造者所說,它應該在其 validate() 方法內部執行自己的所有驗證。在本示例中,為了清楚起見,我們將驗證的某些部分放在了測試方法內部。隨著您更加熟練地使用模擬對象,對于將多少驗證職責代理給模擬對象,您將會深有體會。

內部類魔法
在清單 6 中,我們使用了 AtmGui 的匿名內部子類來覆蓋 createTransaction 方法。因為我們只需要覆蓋一個簡單的方法,所以這是實現我們目標的簡明方法。假如我們覆蓋多個方法或在許多測試之間共享 AtmGui 子類,那么創建一個完整的(非匿名)成員類是值得的。

我們還使用了實例變量來存儲對模擬對象的引用。這是在測試方法和非凡化類之間共享數據的最簡單方法。這是可以接受的,因為我們的測試框架不是多線程的或可重入的。(假如它是多線程的或可重入的,則必須用 synchronized 塊保護我們自己。)

最后,我們將模擬對象本身定義為測試類的專用內部類 ? 這通常是一種便利的方法,因為將模擬對象就放在使用它的測試代碼旁邊會更加清楚,又因為內部類有權訪問包含它們的類的實例變量。

小心不出大錯
因為我們覆蓋了工廠方法來編寫這個測試,所以其結果是:我們的測試不再包括任何原始創建代碼(現在它在基類的工廠方法內部)。添加確實包括該代碼的測試也許是有益的。這與調用基類的工廠方法并斷言返回對象具有正確類型一樣簡單。例如:

AtmGui atm = new AtmGui();
Transaction t = atm.createTransaction();
assertTrue(!(t instanceof MockTransaction));

注:相反,assertTrue(t instanceof Transaction) 不能滿足,因為 MockTransaction 也是 Transaction。

從工廠方法到抽象工廠
此時,您可能很想更進一步并用成熟的抽象工廠對象替換工廠方法,如 Erich Gamma 等人在設計模式中具體描述的那樣。(請參閱參考資料)。實際上,許多人已經用工廠對象來著手這種方法,而不是用工廠方法 ? 我們以前是這樣做的,但很快就放棄了。

將第三種對象類型(角色)引入系統會有一些潛在的缺點:

它增加了復雜性,而沒有相應地增加功能。

它會迫使您更改目標對象的公用接口。假如必須傳入抽象工廠對象,那么您必須添加一個新的公用構造函數或賦值(mutator)方法。

許多語言對于“工廠”這一概念都附有一些約定,它們會使您誤入歧途。例如,在 Java 語言中,工廠通常作為靜態方法實現;在這種情況下,這是不合適的。
請記住,本練習的宗旨是使對象更易于測試。通常,用于可測性的設計可以將對象的 API 推向一種更清楚更模塊化的狀態。但它會走得太遠。測試驅動的設計更改不應該污染原始對象的公用接口。

在 ATM 示例中,對于產品代碼,AtmGui 對象始終只產生一種類型的 Transaction 對象(實際類型)。測試代碼希望它產生另一種類型的對象(模擬對象)。但強迫公用 API 適應工廠對象或抽象工廠(只因為測試代碼要求它這樣)是錯誤的設計。假如產品代碼無需實例化該合作者的多個類型,那么添加該功能將使最終的設計不必要地變得難于理解。

參考資料

由 Tim Mackinnon、Steve Freeman 和 Philip Craig 合著的文章“Endo-Testing: Unit Testing with Mock Objects”介紹了術語模擬對象。

Mock Objects Project 是支持模擬對象實現的框架。

工廠方法和抽象工廠設計模式的來源是由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides(也稱為四人組(Gang of Four))合著的 Design Patterns: Elements of Reusable Object-Oriented Software(Addison-Wesley,1997 年)。

假如您不熟悉設計模式,那么您還需要學習這個由兩部分組成的免費教程:
Java design patterns 101(developerWorks,2002 年 1 月)介紹了模式。
Java design patterns 201(developerWorks,2002 年 4 月)介紹了四人組未描述到的其它模式。

請閱讀在線文章 The Factory Method Design Pattern。

由 Martin Fowler 維護的 Refactoring Home Page 是優秀程序員的主要參考資料。

另外,由 Martin Fowler 編寫的 Refactoring: Improving the Design of Existing Code(Addison-Wesley,1999 年)值得一讀。

JUnit 是最流行的 Java 語言的單元測試框架。

請參考 Purple Technology 的 XP 和重構參考資料列表。

由 XP 教練和 Java 開發人員 Roy Miller 合著的專欄文章 Demystifying Extreme Programming 洞察了這個方法,其中,測試是要害組件。請務必訪問一下附隨的論壇。

Nicholas Lesiecki 的“Test Flexibly with AspectJ and mock objects”(developerWorks,2002 年 5 月)具體地描述了在單元測試時如何使用 AspectJ 和模擬對象。

由 Eric Allen 著的“Diagnosing Java code: Unit tests and automated code analysis working together”(developerWorks,2002 年 10 月)研究了單元測試和靜態分析之間的關系。

WebSphere 開發者園地中“Application Quality Assurance: Unit Testing”一文研究了使用 JUnit 的單元測試。

WebSphere 開發者園地中“Debugging and Unit-Testing Server-Side Web Applications”一文也描述了包括交互式調試和迭代單元測試的服務器端 Web 開發的方案。

在 developerWorks Java 技術專區可找到數百篇有關 Java 技術的文章和教程。

關于作者
Alexander Day Chaffee 是 Purple Technology 的創辦人,該公司提供了 Java 語言、極端編程和開放源碼咨詢和培訓。他治理 jGuru 的 Servlets FAQ。作為 EarthWeb 的軟件工程主管,Alex 與人共同創建了 Gamelan,這是 Java 社區的正式目錄。可以通過 alex@jguru.com 與他聯系。

William Pietri 的父親是一位系統分析師和企業家,William 在十三歲時就開始利用計算機賺取午餐費。從那以后,他幾乎從事過技術領域的各個方面,從技術支持到系統治理到軟件工程到用戶界面設計。他是 Scissor(技術咨詢公司)的創辦人。可以通過 william@scissor.com 與 William 聯系。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 安徽省| 宁国市| 乌鲁木齐县| 分宜县| 靖宇县| 略阳县| 伊金霍洛旗| 安泽县| 汨罗市| 噶尔县| 迁安市| 兰溪市| 新密市| 湖南省| 蕲春县| 泰宁县| 法库县| 克拉玛依市| 云南省| 莱阳市| 开阳县| 广德县| 新化县| 澄江县| 五家渠市| 江安县| 安仁县| 莎车县| 长垣县| 兴文县| 资源县| 乐陵市| 静安区| 岱山县| 岚皋县| 方正县| 东海县| 常宁市| 徐闻县| 亳州市| 平顺县|