轉(zhuǎn)—使用模仿對(duì)象Mock object進(jìn)行單元測(cè)試

          使用模仿對(duì)象Mock object進(jìn)行單元測(cè)試


          NetReptile推薦 [2005-2-12]
          出處:IBM DW中國(guó)
          作者:Alexander Day Chaffe
           

          模仿對(duì)象(Mock object)是為起中介者作用的對(duì)象編寫單元測(cè)試的有用方法。測(cè)試對(duì)象調(diào)用模仿域?qū)ο螅ㄋ粩嘌砸哉_的次序用期望的參數(shù)調(diào)用了正確的方法),而不是調(diào)用實(shí)際域?qū)ο蟆H欢?dāng)測(cè)試對(duì)象必須創(chuàng)建域?qū)ο髸r(shí),我們面臨一個(gè)問(wèn)題。測(cè)試對(duì)象如何知道創(chuàng)建模仿域?qū)ο螅皇莿?chuàng)建實(shí)際域?qū)ο竽兀吭诒疚闹校浖檰?wèn) Alexander Day Chaffee 和 William Pietri 將演示一種重構(gòu)技術(shù),該技術(shù)根據(jù)工廠方法設(shè)計(jì)模式來(lái)創(chuàng)建模仿對(duì)象。

          單元測(cè)試已作為軟件開發(fā)的“最佳實(shí)踐”被普遍接受。當(dāng)編寫對(duì)象時(shí),還必須提供一個(gè)自動(dòng)化測(cè)試類,該類包含測(cè)試該對(duì)象性能的方法、用各種參數(shù)調(diào)用其各種公用(public)方法并確保返回值是正確的。

          當(dāng)您正在處理簡(jiǎn)單數(shù)據(jù)或服務(wù)對(duì)象時(shí),編寫單元測(cè)試很簡(jiǎn)單。然而,許多對(duì)象依賴基礎(chǔ)結(jié)構(gòu)的其它對(duì)象或?qū)印.?dāng)開始測(cè)試這些對(duì)象時(shí),實(shí)例化這些合作者(collaborator)通常是昂貴的、不切實(shí)際的或效率低的。

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

          夾在中間的模仿


          使用模仿對(duì)象進(jìn)行測(cè)試的常用編碼樣式是:

          · 創(chuàng)建模仿對(duì)象的實(shí)例

          · 設(shè)置模仿對(duì)象中的狀態(tài)和期望值

          · 將模仿對(duì)象作為參數(shù)來(lái)調(diào)用域代碼

          · 驗(yàn)證模仿對(duì)象中的一致性

          雖然這種模式對(duì)于許多情況都非常有效,但模仿對(duì)象有時(shí)不能被傳遞到正在測(cè)試的對(duì)象。而設(shè)計(jì)該對(duì)象是為了創(chuàng)建、查找或獲得其合作者。

          例如,測(cè)試對(duì)象可能需要獲得對(duì)Enterprise JavaBean(EJB)組件或遠(yuǎn)程對(duì)象的引用。或者,測(cè)試對(duì)象會(huì)使用具有副作用的對(duì)象,如刪除文件的File對(duì)象,而在單元測(cè)試中不希望有這些副作用。

          根據(jù)常識(shí),我們知道這種情形下可以嘗試重構(gòu)對(duì)象,使之更便于測(cè)試。例如,可以更改方法簽名,以便傳入合作者對(duì)象。

          在 Nicholas Lesiecki 的文章“Test flexibly with AspectJ and mock objects”中,他指出重構(gòu)不一定總是合意的,也不一定總是產(chǎn)生更清晰或更容易理解的代碼。在許多情況下,更改方法簽名以使合作者成為參數(shù)將會(huì)在方法的原始調(diào)用者內(nèi)部產(chǎn)生混淆的、未經(jīng)試驗(yàn)的代碼混亂。

          問(wèn)題的關(guān)鍵是該對(duì)象“在里面”獲得這些對(duì)象。任何解決方案都必須應(yīng)用于這個(gè)創(chuàng)建代碼的所有出現(xiàn)。為了解決這個(gè)問(wèn)題,Lesiecki 使用了查找方式或創(chuàng)建方式。在這個(gè)解決方案中,執(zhí)行查找的代碼被返回模仿對(duì)象的代碼自動(dòng)替換。

          因?yàn)?AspectJ 對(duì)于某些情況不是選項(xiàng),所以我們?cè)诒疚闹刑峁┝艘粋€(gè)替代方法。因?yàn)樵诟旧线@是重構(gòu),所以我們將遵循 Martin Fowler 在他創(chuàng)新的書籍“Refactoring: Improving the Design of Existing Code”(請(qǐng)參閱參考資料)中建立的表達(dá)約定。(我們的代碼基于 JUnit — Java 編程的最流行的單元測(cè)試框架,盡管它決不是 JUnit 特定的。)

          重構(gòu):抽取和覆蓋工廠方法


          重構(gòu)是一種代碼更改,它使原始功能保持不變,但更改代碼設(shè)計(jì),使它變得更清晰、更有效且更易于測(cè)試。本節(jié)將循序漸進(jìn)地描述“抽取”和“覆蓋”工廠方法重構(gòu)。

          問(wèn)題:正在測(cè)試的對(duì)象創(chuàng)建了合作者對(duì)象。必須用模仿對(duì)象替換這個(gè)合作者。

          重構(gòu)之前的代碼:

          class Application {
          ...
            public void run() {
              View v = new View();
              v.display();
          ...


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

          重構(gòu)之后的代碼:

          class Application {
          ...
            public void run() {
              View v = createView();
              v.display();
          ...
            protected View createView() {
              return new View();
            }
          ...
          }


          該重構(gòu)啟用清單1中所示的單元測(cè)試代碼:

          清單 1. 單元測(cè)試代碼

          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);
              }
            }
          }


          角色

          該設(shè)計(jì)引入了由系統(tǒng)中的對(duì)象扮演的下列角色:

          · 目標(biāo)對(duì)象:正在測(cè)試的對(duì)象

          · 合作者對(duì)象:由目標(biāo)對(duì)象創(chuàng)建或獲取的對(duì)象

          · 模仿對(duì)象:遵循模仿對(duì)象模式的合作者的子類(或?qū)崿F(xiàn))

          · 特殊化對(duì)象:覆蓋創(chuàng)建方法以返回模仿對(duì)象而不是合作者的目標(biāo)的子類

          技巧

          重構(gòu)由許多小的技術(shù)性步驟組成。這些步驟統(tǒng)稱為技巧。如果您象按照食譜那樣嚴(yán)格遵循這些技術(shù),那么您在學(xué)習(xí)重構(gòu)時(shí)應(yīng)該沒(méi)有太大的麻煩。

          標(biāo)識(shí)創(chuàng)建或獲取合作者的代碼的所有出現(xiàn)。

          將抽取方法重構(gòu)應(yīng)用于這個(gè)創(chuàng)建代碼,創(chuàng)建工廠方法(在Fowler書籍的第110頁(yè)中討論;有關(guān)更多信息,請(qǐng)參閱參考資料一節(jié))。

          確保目標(biāo)對(duì)象及其子類可以訪問(wèn)工廠方法。(在 Java 語(yǔ)言中,使用 protected 關(guān)鍵字)。

          在測(cè)試代碼中,創(chuàng)建模仿對(duì)象且實(shí)現(xiàn)與合作者相同的接口。

          在測(cè)試代碼中,創(chuàng)建擴(kuò)展(專用于)目標(biāo)對(duì)象的特殊化對(duì)象。

          在特殊化對(duì)象中,覆蓋創(chuàng)建方法以返回為測(cè)試提供的模仿對(duì)象。

          可選的:創(chuàng)建單元測(cè)試以確保原始目標(biāo)對(duì)象的工廠方法仍返回正確的非模仿對(duì)象。

          示例:ATM

          設(shè)想您正在編寫用于銀行自動(dòng)柜員機(jī)(Automatic Teller Machine)的測(cè)試。其中一個(gè)測(cè)試可能類似于清單 2:

          清單 2. 初始單元測(cè)試,在模仿對(duì)象引入之前:

          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 類內(nèi)部的匹配代碼可能類似于清單 3:

          清單 3. 產(chǎn)品代碼,在重構(gòu)之前:

          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();
            }


          該方法將起作用,遺憾的是,它有一個(gè)副作用:支票帳戶余額比測(cè)試開始時(shí)少,這使得其它測(cè)試變得更困難。有一些解決這種困難的方法,但它們都會(huì)增加測(cè)試的復(fù)雜性。更糟的是,該方法還需要對(duì)管理貨幣的系統(tǒng)進(jìn)行三次往返。

          要修正這個(gè)問(wèn)題,第一步是重構(gòu) AtmGui 以允許我們用模仿事務(wù)替換實(shí)際事務(wù),如清單 4 中所示(比較粗體的源代碼以查看我們正在更改什么):

          清單 4. 重構(gòu)

          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();
            }


          后退到測(cè)試類內(nèi)部,我們將 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);
              }
            }


          最后,我們可以重寫測(cè)試,以便被測(cè)試的對(duì)象使用 MockTransaction 類,而不是使用實(shí)際類,如清單 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();
          }


          該解決方案產(chǎn)生了一個(gè)稍長(zhǎng)的測(cè)試,但該測(cè)試只關(guān)注正在測(cè)試的類的直接行為,而不是 ATM 接口之外整個(gè)系統(tǒng)的行為。也就是說(shuō),我們不再檢查測(cè)試帳戶的最終余額是否正確;我們將在對(duì) Transaction 對(duì)象的單元測(cè)試中檢查該函數(shù),而不是在對(duì) AtmGui 對(duì)象的單元測(cè)試中。

          注:根據(jù)模仿對(duì)象的創(chuàng)造者所說(shuō),它應(yīng)該在其 validate() 方法內(nèi)部執(zhí)行自己的所有驗(yàn)證。在本示例中,為了清晰起見(jiàn),我們將驗(yàn)證的某些部分放在了測(cè)試方法內(nèi)部。隨著您更加熟練地使用模仿對(duì)象,對(duì)于將多少驗(yàn)證職責(zé)代理給模仿對(duì)象,您將會(huì)深有體會(huì)。

          內(nèi)部類魔法

          在清單 6 中,我們使用了 AtmGui 的匿名內(nèi)部子類來(lái)覆蓋 createTransaction 方法。因?yàn)槲覀冎恍枰采w一個(gè)簡(jiǎn)單的方法,所以這是實(shí)現(xiàn)我們目標(biāo)的簡(jiǎn)明方法。如果我們覆蓋多個(gè)方法或在許多測(cè)試之間共享 AtmGui 子類,那么創(chuàng)建一個(gè)完整的(非匿名)成員類是值得的。

          我們還使用了實(shí)例變量來(lái)存儲(chǔ)對(duì)模仿對(duì)象的引用。這是在測(cè)試方法和特殊化類之間共享數(shù)據(jù)的最簡(jiǎn)單方法。這是可以接受的,因?yàn)槲覀兊臏y(cè)試框架不是多線程的或可重入的。(如果它是多線程的或可重入的,則必須用 synchronized 塊保護(hù)我們自己。)

          最后,我們將模仿對(duì)象本身定義為測(cè)試類的專用內(nèi)部類 — 這通常是一種便利的方法,因?yàn)閷⒛7聦?duì)象就放在使用它的測(cè)試代碼旁邊會(huì)更加清楚,又因?yàn)閮?nèi)部類有權(quán)訪問(wèn)包含它們的類的實(shí)例變量。

          小心不出大錯(cuò)

          因?yàn)槲覀兏采w了工廠方法來(lái)編寫這個(gè)測(cè)試,所以其結(jié)果是:我們的測(cè)試不再包括任何原始創(chuàng)建代碼(現(xiàn)在它在基類的工廠方法內(nèi)部)。添加確實(shí)包括該代碼的測(cè)試也許是有益的。這與調(diào)用基類的工廠方法并斷言返回對(duì)象具有正確類型一樣簡(jiǎn)單。例如:

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


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

          從工廠方法到抽象工廠


          此時(shí),您可能很想更進(jìn)一步并用成熟的抽象工廠對(duì)象替換工廠方法,如 Erich Gamma 等人在設(shè)計(jì)模式中詳細(xì)描述的那樣。(請(qǐng)參閱參考資料)。實(shí)際上,許多人已經(jīng)用工廠對(duì)象來(lái)著手這種方法,而不是用工廠方法 — 我們以前是這樣做的,但很快就放棄了。

          將第三種對(duì)象類型(角色)引入系統(tǒng)會(huì)有一些潛在的缺點(diǎn):

          它增加了復(fù)雜性,而沒(méi)有相應(yīng)地增加功能。

          它會(huì)迫使您更改目標(biāo)對(duì)象的公用接口。如果必須傳入抽象工廠對(duì)象,那么您必須添加一個(gè)新的公用構(gòu)造函數(shù)或賦值(mutator)方法。

          許多語(yǔ)言對(duì)于“工廠”這一概念都附有一些約定,它們會(huì)使您誤入歧途。例如,在 Java 語(yǔ)言中,工廠通常作為靜態(tài)方法實(shí)現(xiàn);在這種情況下,這是不合適的。

          請(qǐng)記住,本練習(xí)的宗旨是使對(duì)象更易于測(cè)試。通常,用于可測(cè)性的設(shè)計(jì)可以將對(duì)象的 API 推向一種更清晰更模塊化的狀態(tài)。但它會(huì)走得太遠(yuǎn)。測(cè)試驅(qū)動(dòng)的設(shè)計(jì)更改不應(yīng)該污染原始對(duì)象的公用接口。

          在 ATM 示例中,對(duì)于產(chǎn)品代碼,AtmGui 對(duì)象始終只產(chǎn)生一種類型的 Transaction 對(duì)象(實(shí)際類型)。測(cè)試代碼希望它產(chǎn)生另一種類型的對(duì)象(模仿對(duì)象)。但強(qiáng)迫公用 API 適應(yīng)工廠對(duì)象或抽象工廠(只因?yàn)闇y(cè)試代碼要求它這樣)是錯(cuò)誤的設(shè)計(jì)。如果產(chǎn)品代碼無(wú)需實(shí)例化該合作者的多個(gè)類型,那么添加該功能將使最終的設(shè)計(jì)不必要地變得難于理解。

          posted on 2006-02-17 09:18 扭轉(zhuǎn)乾坤 閱讀(310) 評(píng)論(0)  編輯  收藏 所屬分類: JAVA使用技巧

          <2025年8月>
          272829303112
          3456789
          10111213141516
          17181920212223
          24252627282930
          31123456

          導(dǎo)航

          統(tǒng)計(jì)

          常用鏈接

          留言簿(2)

          隨筆分類(31)

          隨筆檔案(30)

          文章分類(32)

          文章檔案(33)

          相冊(cè)

          PHP小站-首頁(yè)

          搜索

          積分與排名

          最新評(píng)論

          閱讀排行榜

          評(píng)論排行榜

          主站蜘蛛池模板: 洛隆县| 沧源| 峨边| 克东县| 利川市| 奉贤区| 平邑县| 鄂伦春自治旗| 祁门县| 彭山县| 清原| 阿瓦提县| 全椒县| 扎赉特旗| 华容县| 犍为县| 湖南省| 南乐县| 延吉市| 成武县| 克什克腾旗| 枝江市| 瑞安市| 财经| 武宣县| 乌拉特中旗| 信丰县| 同心县| 虹口区| 大名县| 黄石市| 西安市| 德清县| 从江县| 五寨县| 姜堰市| 南召县| 新龙县| 乌恰县| 邢台市| 旅游|