您的位置:軟件測(cè)試 > 開(kāi)源軟件測(cè)試 > 開(kāi)源單元測(cè)試工具 >
用EasyMock更輕松地測(cè)試
作者:網(wǎng)絡(luò)轉(zhuǎn)載 發(fā)布時(shí)間:[ 2013/2/21 13:56:57 ] 推薦標(biāo)簽:

設(shè)置預(yù)期

EasyMock 不只是能夠用固定的結(jié)果響應(yīng)固定的輸入。它還可以檢查輸入是否符合預(yù)期。例如,假設(shè) toEuros() 方法有一個(gè) bug(見(jiàn)清單 5),它返回以歐元為單位的結(jié)果,但是獲取的是加拿大元的匯率。這會(huì)讓客戶發(fā)一筆意外之財(cái)或遭受重大損失。

清單 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;
        }
    }
}


但是,不需要為此編寫另一個(gè)測(cè)試。清單 4 中的 testToEuros 能夠捕捉到這個(gè) bug。當(dāng)對(duì)這段代碼運(yùn)行清單 4 中的測(cè)試時(shí),測(cè)試會(huì)失敗并顯示以下錯(cuò)誤消息:

"java.lang.AssertionError:
  Unexpected method call getRate("USD", "CAD"):
    getRate("USD", "EUR"): expected: 1, actual: 0".


注意,這并不是我設(shè)置的斷言。EasyMock 注意到我傳遞的參數(shù)不符合測(cè)試用例。

在默認(rèn)情況下,EasyMock 只允許測(cè)試用例用指定的參數(shù)調(diào)用指定的方法。但是,有時(shí)候這有點(diǎn)兒太嚴(yán)格了,所以有辦法放寬這一限制。例如,假設(shè)希望允許把任何字符串傳遞給 getRate() 方法,而不于 USD 和 EUR。那么,可以指定 EasyMock.anyObject() 而不是顯式的字符串,如下所示:

EasyMock.expect(mock.getRate(
       (String) EasyMock.anyObject(),
       (String) EasyMock.anyObject())).andReturn(1.5);


還可以更挑剔一點(diǎn)兒,通過(guò)指定 EasyMock.notNull() 只允許非 null 字符串:

EasyMock.expect(mock.getRate(
        (String) EasyMock.notNull(),
        (String) EasyMock.notNull())).andReturn(1.5);


靜態(tài)類型檢查會(huì)防止把非 String 對(duì)象傳遞給這個(gè)方法。但是,現(xiàn)在允許傳遞 USD 和 EUR 之外的其他 String。還可以通過(guò) EasyMock.matches() 使用更顯式的正則表達(dá)式。下面指定需要一個(gè)三字母的大寫 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 為基本數(shù)據(jù)類型提供相似的方法:

    EasyMock.anyInt()
    EasyMock.anyShort()
    EasyMock.anyByte()
    EasyMock.anyLong()
    EasyMock.anyFloat()
    EasyMock.anyDouble()
    EasyMock.anyBoolean()

對(duì)于數(shù)字類型,還可以使用 EasyMock.lt(x) 接受小于 x 的任何值,或使用 EasyMock.gt(x) 接受大于 x 的任何值。

在檢查一系列預(yù)期時(shí),可以捕捉一個(gè)方法調(diào)用的結(jié)果或參數(shù),然后與傳遞給另一個(gè)方法調(diào)用的值進(jìn)行比較。后,通過(guò)定義定制的匹配器,可以檢查參數(shù)的任何細(xì)節(jié),但是這個(gè)過(guò)程比較復(fù)雜。但是,對(duì)于大多數(shù)測(cè)試,EasyMock.anyInt()、EasyMock.matches() 和 EasyMock.eq() 這樣的基本匹配器已經(jīng)足夠了。

嚴(yán)格的 mock 和次序檢查

EasyMock 不僅能夠檢查是否用正確的參數(shù)調(diào)用預(yù)期的方法。它還可以檢查是否以正確的次序調(diào)用這些方法,而且只調(diào)用了這些方法。在默認(rèn)情況下,不執(zhí)行這種檢查。要想啟用它,應(yīng)該在測(cè)試方法末尾調(diào)用 EasyMock.verify(mock)。例如,如果 toEuros() 方法不只一次調(diào)用 getRate(),清單 6 會(huì)失敗。

清單 6. 檢查是否只調(diào)用 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() :必須用指定的參數(shù)調(diào)用所有預(yù)期的方法。但是,不考慮調(diào)用這些方法的次序。調(diào)用未預(yù)期的方法會(huì)導(dǎo)致測(cè)試失敗。

    Strict — EasyMock.createStrictMock() :必須以指定的次序用預(yù)期的參數(shù)調(diào)用所有預(yù)期的方法。調(diào)用未預(yù)期的方法會(huì)導(dǎo)致測(cè)試失敗。

    Nice — EasyMock.createNiceMock() :必須以任意次序用指定的參數(shù)調(diào)用所有預(yù)期的方法。調(diào)用未預(yù)期的方法不會(huì) 導(dǎo)致測(cè)試失敗。Nice mock 為沒(méi)有顯式地提供 mock 的方法提供合理的默認(rèn)值。返回?cái)?shù)字的方法返回 0,返回布爾值的方法返回 false。返回對(duì)象的方法返回 null。

檢查調(diào)用方法的次序和次數(shù)對(duì)于大型接口和大型測(cè)試更有意義。例如,請(qǐng)考慮 org.xml.sax.ContentHandler 接口。如果要測(cè)試一個(gè) XML 解析器,希望輸入文檔并檢查解析器是否以正確的次序調(diào)用 ContentHandler 中正確的方法。例如,請(qǐng)考慮清單 7 中的簡(jiǎn)單 XML 文檔:

清單 7. 簡(jiǎn)單的 XML 文檔

    
<root>
  Hello World!
</root>


根據(jù) SAX 規(guī)范,在解析器解析文檔時(shí),它應(yīng)該按以下次序調(diào)用這些方法:

    setDocumentLocator()
    startDocument()
    startElement()
    characters()
    endElement()
    endDocument()

但是,更有意思的是,對(duì) setDocumentLocator() 的調(diào)用是可選的;解析器可以多次調(diào)用 characters()。它們不需要在一次調(diào)用中傳遞盡可能多的連續(xù)文本,實(shí)際上大多數(shù)解析器不這么做。即使是對(duì)于清單 7 這樣的簡(jiǎn)單文檔,也很難用傳統(tǒng)的方法測(cè)試 XML 解析器,但是 EasyMock 大大簡(jiǎn)化了這個(gè)任務(wù),見(jiàn)清單 8:

清單 8. 測(cè)試 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>   Hello World! </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);
    }
}


這個(gè)測(cè)試展示了幾種新技巧。首先,它使用一個(gè) strict mock,因此要求符合指定的次序。例如,不希望解析器在調(diào)用 startDocument() 之前調(diào)用 endDocument()。

第二,要測(cè)試的所有方法都返回 void。這意味著不能把它們作為參數(shù)傳遞給 EasyMock.expect()(像對(duì) getRate() 所做的)。(EasyMock 在許多方面能夠 “欺騙” 編譯器,但是還不足以讓編譯器相信 void 是有效的參數(shù)類型)。因此,要在 mock 上調(diào)用 void 方法,由 EasyMock 捕捉結(jié)果。如果需要修改預(yù)期的細(xì)節(jié),那么在調(diào)用 mock 方法之后立即調(diào)用 EasyMock.expectLastCall()。另外注意,不能作為預(yù)期參數(shù)傳遞任何 String、int 和數(shù)組。必須先用 EasyMock.eq() 包裝它們,這樣才能在預(yù)期中捕捉它們的值。

清單 8 使用 EasyMock.expectLastCall() 調(diào)整預(yù)期的方法調(diào)用次數(shù)。在默認(rèn)情況下,預(yù)期的方法調(diào)用次數(shù)是一次。但是,我通過(guò)調(diào)用 .times(0, 1) 把 setDocumentLocator() 設(shè)置為可選的。這指定調(diào)用此方法的次數(shù)必須是零次或一次。當(dāng)然,可以根據(jù)需要把預(yù)期的方法調(diào)用次數(shù)設(shè)置為任何范圍,比如 1-10 次、3-30 次。對(duì)于 characters(),我實(shí)際上不知道將調(diào)用它多少次,但是知道必須至少調(diào)用一次,所以對(duì)它使用 .atLeastOnce()。如果這是非 void 方法,可以對(duì)預(yù)期直接應(yīng)用 times(0, 1) 和 atLeastOnce()。但是,因?yàn)檫@些方法返回 void,所以必須通過(guò) EasyMock.expectLastCall() 設(shè)置它們。

后注意,這里對(duì) characters() 的參數(shù)使用了 EasyMock.anyObject() 和 EasyMock.anyInt()。這考慮到了解析器向 ContentHandler 傳遞文本的各種方式。

mock 和真實(shí)性

有必要使用 EasyMock 嗎?其實(shí),手工編寫的 mock 類也能夠?qū)崿F(xiàn) EasyMock 的功能,但是手工編寫的類只能適用于某些項(xiàng)目。例如,對(duì)于 清單 3,手工編寫一個(gè)使用匿名內(nèi)部類的 mock 也很容易,代碼很緊湊,對(duì)于不熟悉 EasyMock 的開(kāi)發(fā)人員可讀性可能更好。但是,它是一個(gè)專門為本文構(gòu)造的簡(jiǎn)單示例。在為 org.w3c.dom.Node(25 個(gè)方法)或 java.sql.ResultSet(139 個(gè)方法而且還在增加)這樣的大型接口創(chuàng)建 mock 時(shí),EasyMock 能夠大大節(jié)省時(shí)間,以低的成本創(chuàng)建更短更可讀的代碼。

后,提出一條警告:使用 mock 對(duì)象可能做得太過(guò)分?赡馨烟嗟臇|西替換為 mock,導(dǎo)致即使在代碼質(zhì)量很差的情況下,測(cè)試仍然總是能夠通過(guò)。替換為 mock 的東西越多,接受測(cè)試的東西越少。依賴庫(kù)以及方法與其調(diào)用的方法之間的交互中可能存在許多 bug。把依賴項(xiàng)替換為 mock 會(huì)隱藏許多實(shí)際上可能發(fā)現(xiàn)的 bug。在任何情況下,mock 都不應(yīng)該是您的第一選擇。如果能夠使用真實(shí)的依賴項(xiàng),應(yīng)該這么做。mock 是真實(shí)類的粗糙的替代品。但是,如果由于某種原因無(wú)法用真實(shí)的類可靠且自動(dòng)地進(jìn)行測(cè)試,那么用 mock 進(jìn)行測(cè)試肯定比根本不測(cè)試強(qiáng)。

上一頁(yè)12下一頁(yè)
軟件測(cè)試工具 | 聯(lián)系我們 | 投訴建議 | 誠(chéng)聘英才 | 申請(qǐng)使用列表 | 網(wǎng)站地圖
滬ICP備07036474 2003-2017 版權(quán)所有 上海澤眾軟件科技有限公司 Shanghai ZeZhong Software Co.,Ltd