qileilove

          blog已經轉移至github,大家請訪問 http://qaseven.github.io/

          使用EasyMock更輕松地進行測試

          測試驅動開發是軟件開發的重要部分。如果代碼不進行測試,就是不可靠的。所有代碼都必須測試,而且理想情況下應該在編寫代碼之前編寫測試。但是,有些東西容易測試,有些東西不容易。如果要編寫一個代表貨幣值的簡單的類,那么很容易測試把 $1.23 和 $2.8 相加是否能夠得出 $4.03,而不是 $3.03 或 $4.029999998。測試是否不會出現 $7.465 這樣的貨幣值也不太困難。但是,如何測試把 $7.50 轉換為 €5.88 的方法呢(尤其是在通過連接數據庫查詢隨時變動的匯率信息的情況下)?在每次運行程序時,amount.toEuros() 的正確結果都可能有變化。
            答案是 mock 對象。測試并不通過連接真正的服務器來獲取最新的匯率信息,而是連接一個 mock 服務器,它總是返回相同的匯率。這樣就可以得到可預測的結果,可以根據它進行測試。畢竟,測試的目標是 toEuros() 方法中的邏輯,而不是服務器是否發送正確的值。(那是構建服務器的開發人員要操心的事)。這種 mock 對象有時候稱為 fake。
            mock 對象還有助于測試錯誤條件。例如,如果 toEuros() 方法試圖獲取最新的匯率,但是網絡中斷了,那么會發生什么?可以把以太網線從計算機上拔出來,然后運行測試,但是編寫一個模擬網絡故障的 mock 對象省事得多。
            mock 對象還可以測試類的行為。通過把斷言放在 mock 代碼中,可以檢查要測試的代碼是否在適當的時候把適當的參數傳遞給它的協作者。可以通過 mock 查看和測試類的私有部分,而不需要通過不必要的公共方法公開它們。
            最后,mock 對象有助于從測試中消除依賴項。它們使測試更單元化。涉及 mock 對象的測試中的失敗很可能是要測試的方法中的失敗,不太可能是依賴項中的問題。這有助于隔離問題和簡化調試。
            EasyMock 是一個針對 Java 編程語言的開放源碼 mock 對象庫,可以幫助您快速輕松地創建用于這些用途的 mock 對象。EasyMock 使用動態代理,讓您只用一行代碼就能夠創建任何接口的基本實現。通過添加 EasyMock 類擴展,還可以為類創建 mock。可以針對任何用途配置這些 mock,從方法簽名中的簡單啞參數到檢驗一系列方法調用的多調用測試。
            EasyMock 簡介
            現在通過一個具體示例演示 EasyMock 的工作方式。清單 1 是虛構的 ExchangeRate 接口。與任何接口一樣,接口只說明實例要做什么,而不指定應該怎么做。例如,它并沒有指定從 Yahoo 金融服務、政府還是其他地方獲取匯率數據。
            清單 1. ExchangeRate
            import java.io.IOException;
            public interface ExchangeRate {
            double getRate(String inputCurrency, String outputCurrency) throws IOException;
            }
            清單 2 是假定的 Currency 類的骨架。它實際上相當復雜,很可能包含 bug。(您不必猜了:確實有 bug,實際上有不少)。
            清單 2. Currency 類
          import java.io.IOException;
          public class Currency {
          private String units;
          private long amount;
          private int cents;
          public Currency(double amount, String code) {
          this.units = code;
          setAmount(amount);
          }
          private void setAmount(double amount) {
          this.amount = new Double(amount).longValue();
          this.cents = (int) ((amount * 100.0) % 100);
          }
          public Currency toEuros(ExchangeRate converter) {
          if ("EUR".equals(units)) return this;
          else {
          double input = amount + cents/100.0;
          double rate;
          try {
          rate = converter.getRate(units, "EUR");
          double output = input * rate;
          return new Currency(output, "EUR");
          } catch (IOException ex) {
          return null;
          }
          }
          }
          public boolean equals(Object o) {
          if (o instanceof Currency) {
          Currency other = (Currency) o;
          return this.units.equals(other.units)
          && this.amount == other.amount
          && this.cents == other.cents;
          }
          return false;
          }
          public String toString() {
          return amount + "." + Math.abs(cents) + " " + units;
          }
          }
           Currency 類設計的一些重點可能不容易一下子看出來。匯率是從這個類之外 傳遞進來的,并不是在類內部構造的。因此,很有必要為匯率創建 mock,這樣在運行測試時就不需要與真正的匯率服務器通信。這還使客戶機應用程序能夠使用不同的匯率數據源。
            清單 3 給出一個 JUnit 測試,它檢查在匯率為 1.5 的情況下 $2.50 是否會轉換為 €3.75。使用 EasyMock 創建一個總是提供值 1.5 的ExchangeRate 對象。
            清單 3. CurrencyTest 類
          import junit.framework.TestCase;
          import org.easymock.EasyMock;
          import java.io.IOException;
          public class CurrencyTest extends TestCase {
          public void testToEuros() throws IOException {
          Currency expected = new Currency(3.75, "EUR");
          ExchangeRate mock = EasyMock.createMock(ExchangeRate.class);
          EasyMock.expect(mock.getRate("USD", "EUR")).andReturn(1.5);
          EasyMock.replay(mock);
          Currency actual = testObject.toEuros(mock);
          assertEquals(expected, actual);
          }
          }
            老實說,在我第一次運行 清單 3 時失敗了,測試中經常出現這種問題。但是,我已經糾正了 bug。這就是我們采用 TDD 的原因。
            運行這個測試,它通過了。發生了什么?我們來逐行看看這個測試。首先,構造測試對象和預期的結果:
            Currency testObject = new Currency(2.50, "USD");
            Currency expected = new Currency(3.75, "EUR");
            這不是新東西。
            接下來,通過把 ExchangeRate 接口的 Class 對象傳遞給靜態的 EasyMock.createMock() 方法,創建這個接口的 mock 版本:
            ExchangeRate mock = EasyMock.createMock(ExchangeRate.class);
            這是到目前為止最不可思議的部分。注意,我可沒有編寫實現 ExchangeRate 接口的類。另外,EasyMock.createMock() 方法絕對無法返回ExchangeRate 的實例,它根本不知道這個類型,這個類型是我為本文創建的。即使它能夠通過某種奇跡返回 ExchangeRate,但是如果需要模擬另一個接口的實例,又會怎么樣呢?
            我最初看到這個時也非常困惑。我不相信這段代碼能夠編譯,但是它確實可以。這里的 “黑魔法” 來自 Java 1.3 中引入的 Java 5 泛型和動態代理(見 參考資料)。幸運的是,您不需要了解它的工作方式(發明這些訣竅的程序員確實非常聰明)。
            下一步同樣令人吃驚。為了告訴 mock 期望什么結果,把方法作為參數傳遞給 EasyMock.expect() 方法。然后調用 andReturn() 指定調用這個方法應該得到什么結果:
            EasyMock.expect(mock.getRate("USD", "EUR")).andReturn(1.5);
            EasyMock 記錄這個調用,因此知道以后應該重放什么。
            如果在使用 mock 之前忘了調用 EasyMock.replay(),那么會出現 IllegalStateException 異常和一個沒有什么幫助的錯誤消息:missing behavior definition for the preceding method call。
            接下來,通過調用 EasyMock.replay() 方法,讓 mock 準備重放記錄的數據:
            EasyMock.replay(mock);
            這是讓我比較困惑的設計之一。EasyMock.replay() 不會實際重放 mock。而是重新設置 mock,在下一次調用它的方法時,它將開始重放。
            現在 mock 準備好了,我把它作為參數傳遞給要測試的方法:
            為類創建 mock
            從實現的角度來看,很難為類創建 mock。不能為類創建動態代理。標準的 EasyMock 框架不支持類的 mock。但是,EasyMock 類擴展使用字節碼操作產生相同的效果。您的代碼中采用的模式幾乎完全一樣。只需導入org.easymock.classextension.EasyMock 而不是org.easymock.EasyMock。為類創建 mock 允許把類中的一部分方法替換為 mock,而其他方法保持不變。
            Currency actual = testObject.toEuros(mock);
            最后,檢查結果是否符合預期:
            assertEquals(expected, actual);
            這就完成了。如果有一個需要返回特定值的接口需要測試,就可以快速地創建一個 mock。這確實很容易。ExchangeRate 接口很小很簡單,很容易為它手工編寫 mock 類。但是,接口越大越復雜,就越難為每個單元測試編寫單獨的 mock。通過使用 EasyMock,只需一行代碼就能夠創建 java.sql.ResultSet 或 org.xml.sax.ContentHandler 這樣的大型接口的實現,然后向它們提供運行測試所需的行為。
            測試異常
            mock 最常見的用途之一是測試異常條件。例如,無法簡便地根據需要制造網絡故障,但是可以創建模擬網絡故障的 mock。
            當 getRate() 拋出 IOException 時,Currency 類應該返回 null。清單 4 測試這一點:
            清單 4. 測試方法是否拋出正確的異常
          public void testExchangeRateServerUnavailable() throws IOException {
          ExchangeRate mock = EasyMock.createMock(ExchangeRate.class);
          EasyMock.expect(mock.getRate("USD", "EUR")).andThrow(new IOException());
          EasyMock.replay(mock);
          Currency actual = testObject.toEuros(mock);
          assertNull(actual);
          }
            這里的新東西是 andThrow() 方法。顧名思義,它只是讓 getRate() 方法在被調用時拋出指定的異常。
            可以拋出您需要的任何類型的異常(已檢查、運行時或錯誤),只要方法簽名支持它即可。這對于測試極其少見的條件(例如內存耗盡錯誤或無法找到類定義)或表示虛擬機 bug 的條件(比如 UTF-8 字符編碼不可用)尤其有幫助。
            設置預期
            EasyMock 不只是能夠用固定的結果響應固定的輸入。它還可以檢查輸入是否符合預期。例如,假設 toEuros() 方法有一個 bug(見清單 5),它返回以歐元為單位的結果,但是獲取的是加拿大元的匯率。這會讓客戶發一筆意外之財或遭受重大損失。
            清單 5. 有 bug 的 toEuros() 方法
          public Currency toEuros(ExchangeRate converter) {
          if ("EUR".equals(units)) return this;
          else {
          double input = amount + cents/100.0;
          double rate;
          try {
          rate = converter.getRate(units, "CAD");
          double output = input * rate;
          return new Currency(output, "EUR");
          } catch (IOException e) {
          return null;
          }
          }
          }
            但是,不需要為此編寫另一個測試。清單 4 中的 testToEuros 能夠捕捉到這個 bug。當對這段代碼運行清單 4 中的測試時,測試會失敗并顯示以下錯誤消息:
            "java.lang.AssertionError:
            Unexpected method call getRate("USD", "CAD"):
            getRate("USD", "EUR"): expected: 1, actual: 0".
            注意,這并不是我設置的斷言。EasyMock 注意到我傳遞的參數不符合測試用例。
            在默認情況下,EasyMock 只允許測試用例用指定的參數調用指定的方法。但是,有時候這有點兒太嚴格了,所以有辦法放寬這一限制。例如,假設希望允許把任何字符串傳遞給 getRate() 方法,而不僅限于 USD 和 EUR。那么,可以指定 EasyMock.anyObject() 而不是顯式的字符串,如下所示:
            EasyMock.expect(mock.getRate(
            (String) EasyMock.anyObject(),
            (String) EasyMock.anyObject())).andReturn(1.5);
            還可以更挑剔一點兒,通過指定 EasyMock.notNull() 只允許非 null 字符串:
            EasyMock.expect(mock.getRate(
            (String) EasyMock.notNull(),
            (String) EasyMock.notNull())).andReturn(1.5);
            靜態類型檢查會防止把非 String 對象傳遞給這個方法。但是,現在允許傳遞 USD 和 EUR 之外的其他 String。還可以通過EasyMock.matches() 使用更顯式的正則表達式。下面指定需要一個三字母的大寫 ASCII String:
            EasyMock.expect(mock.getRate(
            (String) EasyMock.matches("[A-Z][A-Z][A-Z]"),
            (String) EasyMock.matches("[A-Z][A-Z][A-Z]"))).andReturn(1.5);
            使用 EasyMock.find() 而不是 EasyMock.matches(),就可以接受任何包含三字母大寫子 String 的 String。 EasyMock 為基本數據類型提供相似的方法:
            EasyMock.anyInt()
            EasyMock.anyShort()
            EasyMock.anyByte()
            EasyMock.anyLong()
            EasyMock.anyFloat()
            EasyMock.anyDouble()
            EasyMock.anyBoolean()
            對于數字類型,還可以使用 EasyMock.lt(x) 接受小于 x 的任何值,或使用 EasyMock.gt(x) 接受大于 x 的任何值。
            在檢查一系列預期時,可以捕捉一個方法調用的結果或參數,然后與傳遞給另一個方法調用的值進行比較。最后,通過定義定制的匹配器,可以檢查參數的任何細節,但是這個過程比較復雜。但是,對于大多數測試,EasyMock.anyInt()、EasyMock.matches() 和 EasyMock.eq() 這樣的基本匹配器已經足夠了。
            嚴格的 mock 和次序檢查
            EasyMock 不僅能夠檢查是否用正確的參數調用預期的方法。它還可以檢查是否以正確的次序調用這些方法,而且只調用了這些方法。在默認情況下,不執行這種檢查。要想啟用它,應該在測試方法末尾調用 EasyMock.verify(mock)。例如,如果 toEuros() 方法不只一次調用getRate(),清單 6 就會失敗。
            清單 6. 檢查是否只調用 getRate() 一次
          public void testToEuros() throws IOException {
          Currency expected = new Currency(3.75, "EUR");
          ExchangeRate mock = EasyMock.createMock(ExchangeRate.class);
          EasyMock.expect(mock.getRate("USD", "EUR")).andReturn(1.5);
          EasyMock.replay(mock);
          Currency actual = testObject.toEuros(mock);
          assertEquals(expected, actual);
          EasyMock.verify(mock);
          }
            EasyMock.verify() 究竟做哪些檢查取決于它采用的操作模式:
            Normal — EasyMock.createMock():必須用指定的參數調用所有預期的方法。但是,不考慮調用這些方法的次序。調用未預期的方法會導致測試失敗。
            Strict — EasyMock.createStrictMock():必須以指定的次序用預期的參數調用所有預期的方法。調用未預期的方法會導致測試失敗。
            Nice — EasyMock.createNiceMock():必須以任意次序用指定的參數調用所有預期的方法。調用未預期的方法不會 導致測試失敗。Nice mock 為沒有顯式地提供 mock 的方法提供合理的默認值。返回數字的方法返回 0,返回布爾值的方法返回 false。返回對象的方法返回 null。
            檢查調用方法的次序和次數對于大型接口和大型測試更有意義。例如,請考慮 org.xml.sax.ContentHandler 接口。如果要測試一個 XML 解析器,希望輸入文檔并檢查解析器是否以正確的次序調用 ContentHandler 中正確的方法。例如,請考慮清單 7 中的簡單 XML 文檔:
            清單 7. 簡單的 XML 文檔
            <root>
            Hello World!
            </root>
            根據 SAX 規范,在解析器解析文檔時,它應該按以下次序調用這些方法:
            setDocumentLocator()
            startDocument()
            startElement()
            characters()
            endElement()
            endDocument()
            但是,更有意思的是,對 setDocumentLocator() 的調用是可選的;解析器可以多次調用 characters()。它們不需要在一次調用中傳遞盡可能多的連續文本,實際上大多數解析器不這么做。即使是對于清單 7 這樣的簡單文檔,也很難用傳統的方法測試 XML 解析器,但是 EasyMock 大大簡化了這個任務,見清單 8:
            清單 8. 測試 XML 解析器
          import java.io.*;
          import org.easymock.EasyMock;
          import org.xml.sax.*;
          import org.xml.sax.helpers.XMLReaderFactory;
          import junit.framework.TestCase;
          public class XMLParserTest extends TestCase {
          private  XMLReader parser;
          protected void setUp() throws Exception {
          parser = XMLReaderFactory.createXMLReader();
          }
          public void testSimpleDoc() throws IOException, SAXException {
          String doc = "<root>\n  Hello World!\n</root>";
          ContentHandler mock = EasyMock.createStrictMock(ContentHandler.class);
          mock.setDocumentLocator((Locator) EasyMock.anyObject());
          EasyMock.expectLastCall().times(0, 1);
          mock.startDocument();
          mock.startElement(EasyMock.eq(""), EasyMock.eq("root"), EasyMock.eq("root"),
          (Attributes) EasyMock.anyObject());
          mock.characters((char[]) EasyMock.anyObject(),
          EasyMock.anyInt(), EasyMock.anyInt());
          EasyMock.expectLastCall().atLeastOnce();
          mock.endElement(EasyMock.eq(""), EasyMock.eq("root"), EasyMock.eq("root"));
          mock.endDocument();
          EasyMock.replay(mock);
          parser.setContentHandler(mock);
          InputStream in = new ByteArrayInputStream(doc.getBytes("UTF-8"));
          parser.parse(new InputSource(in));
          EasyMock.verify(mock);
          }
          }
            這個測試展示了幾種新技巧。首先,它使用一個 strict mock,因此要求符合指定的次序。例如,不希望解析器在調用 startDocument() 之前調用 endDocument()。
            第二,要測試的所有方法都返回 void。這意味著不能把它們作為參數傳遞給 EasyMock.expect()(就像對 getRate() 所做的)。(EasyMock 在許多方面能夠 “欺騙” 編譯器,但是還不足以讓編譯器相信 void 是有效的參數類型)。因此,要在 mock 上調用 void 方法,由 EasyMock 捕捉結果。如果需要修改預期的細節,那么在調用 mock 方法之后立即調用 EasyMock.expectLastCall()。另外注意,不能作為預期參數傳遞任何 String、int 和數組。必須先用 EasyMock.eq() 包裝它們,這樣才能在預期中捕捉它們的值。
            清單 8 使用 EasyMock.expectLastCall() 調整預期的方法調用次數。在默認情況下,預期的方法調用次數是一次。但是,我通過調用.times(0, 1) 把 setDocumentLocator() 設置為可選的。這指定調用此方法的次數必須是零次或一次。當然,可以根據需要把預期的方法調用次數設置為任何范圍,比如 1-10 次、3-30 次。對于 characters(),我實際上不知道將調用它多少次,但是知道必須至少調用一次,所以對它使用 .atLeastOnce()。如果這是非 void 方法,就可以對預期直接應用 times(0, 1) 和 atLeastOnce()。但是,因為這些方法返回 void,所以必須通過 EasyMock.expectLastCall() 設置它們。
            最后注意,這里對 characters() 的參數使用了 EasyMock.anyObject() 和 EasyMock.anyInt()。這考慮到了解析器向 ContentHandler 傳遞文本的各種方式。
            mock 和真實性
            有必要使用 EasyMock 嗎?其實,手工編寫的 mock 類也能夠實現 EasyMock 的功能,但是手工編寫的類只能適用于某些項目。例如,對于 清單 3,手工編寫一個使用匿名內部類的 mock 也很容易,代碼很緊湊,對于不熟悉 EasyMock 的開發人員可讀性可能更好。但是,它是一個專門為本文構造的簡單示例。在為 org.w3c.dom.Node(25 個方法)或 java.sql.ResultSet(139 個方法而且還在增加)這樣的大型接口創建 mock 時,EasyMock 能夠大大節省時間,以最低的成本創建更短更可讀的代碼。
            最后,提出一條警告:使用 mock 對象可能做得太過分。可能把太多的東西替換為 mock,導致即使在代碼質量很差的情況下,測試仍然總是能夠通過。替換為 mock 的東西越多,接受測試的東西就越少。依賴庫以及方法與其調用的方法之間的交互中可能存在許多 bug。把依賴項替換為 mock 會隱藏許多實際上可能發現的 bug。在任何情況下,mock 都不應該是您的第一選擇。如果能夠使用真實的依賴項,就應該這么做。mock 是真實類的粗糙的替代品。但是,如果由于某種原因無法用真實的類可靠且自動地進行測試,那么用 mock 進行測試肯定比根本不測試強。

          posted on 2014-07-10 19:20 順其自然EVO 閱讀(3426) 評論(0)  編輯  收藏 所屬分類: 測試學習專欄

          <2014年7月>
          293012345
          6789101112
          13141516171819
          20212223242526
          272829303112
          3456789

          導航

          統計

          常用鏈接

          留言簿(55)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 柘荣县| 高唐县| 安丘市| 上思县| 井陉县| 平江县| 阜新市| 龙川县| 临猗县| 普宁市| 六盘水市| 靖州| 额尔古纳市| 黎城县| 莒南县| 抚州市| 邹平县| 巨野县| 洪泽县| 武川县| 交城县| 蓬安县| 方城县| 闽侯县| 阿鲁科尔沁旗| 武川县| 红河县| 枞阳县| 景东| 乐至县| 沈丘县| 图木舒克市| 白河县| 玉环县| 曲水县| 巴彦淖尔市| 中山市| 井研县| 札达县| 页游| 泾源县|