設(shè)置預(yù)期
EasyMock 不只是能夠用固定的結(jié)果響應(yīng)固定的輸入。它還可以檢查輸入是否符合預(yù)期。例如,假設(shè) toEuros() 方法有一個 bug(見清單 5),它返回以歐元為單位的結(jié)果,但是獲取的是加拿大元的匯率。這會讓客戶發(fā)一筆意外之財或遭受重大損失。
清單 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。當(dāng)對這段代碼運(yùn)行清單 4 中的測試時,測試會失敗并顯示以下錯誤消息:
"java.lang.AssertionError:
Unexpected method call getRate("USD", "CAD"):
getRate("USD", "EUR"): expected: 1, actual: 0".
注意,這并不是我設(shè)置的斷言。EasyMock 注意到我傳遞的參數(shù)不符合測試用例。
在默認(rèn)情況下,EasyMock 只允許測試用例用指定的參數(shù)調(diào)用指定的方法。但是,有時候這有點(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)兒,通過指定 EasyMock.notNull() 只允許非 null 字符串:
EasyMock.expect(mock.getRate(
(String) EasyMock.notNull(),
(String) EasyMock.notNull())).andReturn(1.5);
靜態(tài)類型檢查會防止把非 String 對象傳遞給這個方法。但是,現(xiàn)在允許傳遞 USD 和 EUR 之外的其他 String。還可以通過 EasyMock.matches() 使用更顯式的正則表達(dá)式。下面指定需要一個三字母的大寫 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()
對于數(shù)字類型,還可以使用 EasyMock.lt(x) 接受小于 x 的任何值,或使用 EasyMock.gt(x) 接受大于 x 的任何值。
在檢查一系列預(yù)期時,可以捕捉一個方法調(diào)用的結(jié)果或參數(shù),然后與傳遞給另一個方法調(diào)用的值進(jìn)行比較。后,通過定義定制的匹配器,可以檢查參數(shù)的任何細(xì)節(jié),但是這個過程比較復(fù)雜。但是,對于大多數(shù)測試,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)該在測試方法末尾調(diào)用 EasyMock.verify(mock)。例如,如果 toEuros() 方法不只一次調(diào)用 getRate(),清單 6 會失敗。
清單 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ù)期的方法會導(dǎo)致測試失敗。
Strict — EasyMock.createStrictMock() :必須以指定的次序用預(yù)期的參數(shù)調(diào)用所有預(yù)期的方法。調(diào)用未預(yù)期的方法會導(dǎo)致測試失敗。
Nice — EasyMock.createNiceMock() :必須以任意次序用指定的參數(shù)調(diào)用所有預(yù)期的方法。調(diào)用未預(yù)期的方法不會 導(dǎo)致測試失敗。Nice mock 為沒有顯式地提供 mock 的方法提供合理的默認(rèn)值。返回數(shù)字的方法返回 0,返回布爾值的方法返回 false。返回對象的方法返回 null。
檢查調(diào)用方法的次序和次數(shù)對于大型接口和大型測試更有意義。例如,請考慮 org.xml.sax.ContentHandler 接口。如果要測試一個 XML 解析器,希望輸入文檔并檢查解析器是否以正確的次序調(diào)用 ContentHandler 中正確的方法。例如,請考慮清單 7 中的簡單 XML 文檔:
清單 7. 簡單的 XML 文檔
<root>
Hello World!
</root>
根據(jù) SAX 規(guī)范,在解析器解析文檔時,它應(yīng)該按以下次序調(diào)用這些方法:
setDocumentLocator()
startDocument()
startElement()
characters()
endElement()
endDocument()
但是,更有意思的是,對 setDocumentLocator() 的調(diào)用是可選的;解析器可以多次調(diào)用 characters()。它們不需要在一次調(diào)用中傳遞盡可能多的連續(xù)文本,實(shí)際上大多數(shù)解析器不這么做。即使是對于清單 7 這樣的簡單文檔,也很難用傳統(tǒng)的方法測試 XML 解析器,但是 EasyMock 大大簡化了這個任務(wù),見清單 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>
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);
}
}
這個測試展示了幾種新技巧。首先,它使用一個 strict mock,因此要求符合指定的次序。例如,不希望解析器在調(diào)用 startDocument() 之前調(diào)用 endDocument()。
第二,要測試的所有方法都返回 void。這意味著不能把它們作為參數(shù)傳遞給 EasyMock.expect()(像對 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ù)是一次。但是,我通過調(diào)用 .times(0, 1) 把 setDocumentLocator() 設(shè)置為可選的。這指定調(diào)用此方法的次數(shù)必須是零次或一次。當(dāng)然,可以根據(jù)需要把預(yù)期的方法調(diào)用次數(shù)設(shè)置為任何范圍,比如 1-10 次、3-30 次。對于 characters(),我實(shí)際上不知道將調(diào)用它多少次,但是知道必須至少調(diào)用一次,所以對它使用 .atLeastOnce()。如果這是非 void 方法,可以對預(yù)期直接應(yīng)用 times(0, 1) 和 atLeastOnce()。但是,因?yàn)檫@些方法返回 void,所以必須通過 EasyMock.expectLastCall() 設(shè)置它們。
后注意,這里對 characters() 的參數(shù)使用了 EasyMock.anyObject() 和 EasyMock.anyInt()。這考慮到了解析器向 ContentHandler 傳遞文本的各種方式。
mock 和真實(shí)性
有必要使用 EasyMock 嗎?其實(shí),手工編寫的 mock 類也能夠?qū)崿F(xiàn) EasyMock 的功能,但是手工編寫的類只能適用于某些項目。例如,對于 清單 3,手工編寫一個使用匿名內(nèi)部類的 mock 也很容易,代碼很緊湊,對于不熟悉 EasyMock 的開發(fā)人員可讀性可能更好。但是,它是一個專門為本文構(gòu)造的簡單示例。在為 org.w3c.dom.Node(25 個方法)或 java.sql.ResultSet(139 個方法而且還在增加)這樣的大型接口創(chuàng)建 mock 時,EasyMock 能夠大大節(jié)省時間,以低的成本創(chuàng)建更短更可讀的代碼。
后,提出一條警告:使用 mock 對象可能做得太過分?赡馨烟嗟臇|西替換為 mock,導(dǎo)致即使在代碼質(zhì)量很差的情況下,測試仍然總是能夠通過。替換為 mock 的東西越多,接受測試的東西越少。依賴庫以及方法與其調(diào)用的方法之間的交互中可能存在許多 bug。把依賴項替換為 mock 會隱藏許多實(shí)際上可能發(fā)現(xiàn)的 bug。在任何情況下,mock 都不應(yīng)該是您的第一選擇。如果能夠使用真實(shí)的依賴項,應(yīng)該這么做。mock 是真實(shí)類的粗糙的替代品。但是,如果由于某種原因無法用真實(shí)的類可靠且自動地進(jìn)行測試,那么用 mock 進(jìn)行測試肯定比根本不測試強(qiáng)。