測(cè)試驅(qū)動(dòng)開(kāi)發(fā)是軟件開(kāi)發(fā)的重要部分。如果代碼不進(jìn)行測(cè)試,就是不可靠的。所有代碼都必須測(cè)試,而且理想情況下應(yīng)該在編寫代碼之前編寫測(cè)試。但是,有些東西容易測(cè)試,有些東西不容易。如果要編寫一個(gè)代表貨幣值的簡(jiǎn)單的類,那么很容易測(cè)試把 $1.23 和 $2.8 相加是否能夠得出 $4.03,而不是 $3.03 或 $4.029999998。測(cè)試是否不會(huì)出現(xiàn) $7.465 這樣的貨幣值也不太困難。但是,如何測(cè)試把 $7.50 轉(zhuǎn)換為 €5.88 的方法呢(尤其是在通過(guò)連接數(shù)據(jù)庫(kù)查詢隨時(shí)變動(dòng)的匯率信息的情況下)?在每次運(yùn)行程序時(shí),
amount.toEuros()
的正確結(jié)果都可能有變化。
答案是
mock 對(duì)象
。測(cè)試并不通過(guò)連接真正的服務(wù)器來(lái)獲取最新的匯率信息,而是連接一個(gè) mock 服務(wù)器,它總是返回相同的匯率。這樣就可以得到可預(yù)測(cè)的結(jié)果,可以根據(jù)它進(jìn)行測(cè)試。畢竟,測(cè)試的目標(biāo)是
toEuros()
方法中的邏輯,而不是服務(wù)器是否發(fā)送正確的值。(那是構(gòu)建服務(wù)器的開(kāi)發(fā)人員要操心的事)。這種 mock 對(duì)象有時(shí)候稱為
fake
。
mock 對(duì)象還有助于測(cè)試錯(cuò)誤條件。例如,如果
toEuros()
方法試圖獲取最新的匯率,但是網(wǎng)絡(luò)中斷了,那么會(huì)發(fā)生什么?可以把以太網(wǎng)線從計(jì)算機(jī)上拔出來(lái),然后運(yùn)行測(cè)試,但是編寫一個(gè)模擬網(wǎng)絡(luò)故障的 mock 對(duì)象省事得多。
mock 對(duì)象還可以測(cè)試類的行為。通過(guò)把斷言放在 mock 代碼中,可以檢查要測(cè)試的代碼是否在適當(dāng)?shù)臅r(shí)候把適當(dāng)?shù)膮?shù)傳遞給它的協(xié)作者??梢酝ㄟ^(guò) mock 查看和測(cè)試類的私有部分,而不需要通過(guò)不必要的公共方法公開(kāi)它們。
最后,mock 對(duì)象有助于從測(cè)試中消除依賴項(xiàng)。它們使測(cè)試更單元化。涉及 mock 對(duì)象的測(cè)試中的失敗很可能是要測(cè)試的方法中的失敗,不太可能是依賴項(xiàng)中的問(wèn)題。這有助于隔離問(wèn)題和簡(jiǎn)化調(diào)試。
EasyMock 是一個(gè)針對(duì) Java 編程語(yǔ)言的開(kāi)放源碼 mock 對(duì)象庫(kù),可以幫助您快速輕松地創(chuàng)建用于這些用途的 mock 對(duì)象。EasyMock 使用動(dòng)態(tài)代理,讓您只用一行代碼就能夠創(chuàng)建任何接口的基本實(shí)現(xiàn)。通過(guò)添加 EasyMock 類擴(kuò)展,還可以為類創(chuàng)建 mock。可以針對(duì)任何用途配置這些 mock,從方法簽名中的簡(jiǎn)單啞參數(shù)到檢驗(yàn)一系列方法調(diào)用的多調(diào)用測(cè)試。
現(xiàn)在通過(guò)一個(gè)具體示例演示 EasyMock 的工作方式。清單 1 是虛構(gòu)的
ExchangeRate
接口。與任何接口一樣,接口只說(shuō)明實(shí)例要做什么,而不指定應(yīng)該怎么做。例如,它并沒(méi)有指定從 Yahoo 金融服務(wù)、政府還是其他地方獲取匯率數(shù)據(jù)。
import java.io.IOException; public interface ExchangeRate { double getRate(String inputCurrency, String outputCurrency) throws IOException; } |
?
清單 2 是假定的
Currency
類的骨架。它實(shí)際上相當(dāng)復(fù)雜,很可能包含 bug。(您不必猜了:確實(shí)有 bug,實(shí)際上有不少)。
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
類設(shè)計(jì)的一些重點(diǎn)可能不容易一下子看出來(lái)。匯率是從這個(gè)類
之外
傳遞進(jìn)來(lái)的,并不是在類內(nèi)部構(gòu)造的。因此,很有必要為匯率創(chuàng)建 mock,這樣在運(yùn)行測(cè)試時(shí)就不需要與真正的匯率服務(wù)器通信。這還使客戶機(jī)應(yīng)用程序能夠使用不同的匯率數(shù)據(jù)源。
清單 3 給出一個(gè) JUnit 測(cè)試,它檢查在匯率為 1.5 的情況下 $2.50 是否會(huì)轉(zhuǎn)換為 €3.75。使用 EasyMock 創(chuàng)建一個(gè)總是提供值 1.5 的
ExchangeRate
對(duì)象。
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); } } |
?
![]() |
|
運(yùn)行這個(gè)測(cè)試,它通過(guò)了。發(fā)生了什么?我們來(lái)逐行看看這個(gè)測(cè)試。首先,構(gòu)造測(cè)試對(duì)象和預(yù)期的結(jié)果:
Currency testObject = new Currency(2.50, "USD"); Currency expected = new Currency(3.75, "EUR"); |
?
這不是新東西。
接下來(lái),通過(guò)把
ExchangeRate
接口的
Class
對(duì)象傳遞給靜態(tài)的
EasyMock.createMock()
方法,創(chuàng)建這個(gè)接口的 mock 版本:
ExchangeRate mock = EasyMock.createMock(ExchangeRate.class); |
?
這是到目前為止最不可思議的部分。注意,我可沒(méi)有編寫實(shí)現(xiàn)
ExchangeRate
接口的類。另外,
EasyMock.createMock()
方法絕對(duì)無(wú)法返回
ExchangeRate
的實(shí)例,它根本不知道這個(gè)類型,這個(gè)類型是我為本文創(chuàng)建的。即使它能夠通過(guò)某種奇跡返回
ExchangeRate
,但是如果需要模擬另一個(gè)接口的實(shí)例,又會(huì)怎么樣呢?
我最初看到這個(gè)時(shí)也非常困惑。我不相信這段代碼能夠編譯,但是它確實(shí)可以。這里的 “黑魔法” 來(lái)自 Java 1.3 中引入的 Java 5 泛型和動(dòng)態(tài)代理(見(jiàn) 參考資料 )。幸運(yùn)的是,您不需要了解它的工作方式(發(fā)明這些訣竅的程序員確實(shí)非常聰明)。
下一步同樣令人吃驚。為了告訴 mock 期望什么結(jié)果,把方法作為參數(shù)傳遞給
EasyMock.expect()
方法。然后調(diào)用
andReturn()
指定調(diào)用這個(gè)方法應(yīng)該得到什么結(jié)果:
EasyMock.expect(mock.getRate("USD", "EUR")).andReturn(1.5); |
?
EasyMock 記錄這個(gè)調(diào)用,因此知道以后應(yīng)該重放什么。
![]() |
|
接下來(lái),通過(guò)調(diào)用
EasyMock.replay()
方法,讓 mock 準(zhǔn)備重放記錄的數(shù)據(jù):
EasyMock.replay(mock); |
?
這是讓我比較困惑的設(shè)計(jì)之一。
EasyMock.replay()
不會(huì)實(shí)際重放 mock。而是重新設(shè)置 mock,在下一次調(diào)用它的方法時(shí),它將開(kāi)始重放。
現(xiàn)在 mock 準(zhǔn)備好了,我把它作為參數(shù)傳遞給要測(cè)試的方法:
![]() |
|
Currency actual = testObject.toEuros(mock); |
?
最后,檢查結(jié)果是否符合預(yù)期:
assertEquals(expected, actual); |
?
這就完成了。如果有一個(gè)需要返回特定值的接口需要測(cè)試,就可以快速地創(chuàng)建一個(gè) mock。這確實(shí)很容易。
ExchangeRate
接口很小很簡(jiǎn)單,很容易為它手工編寫 mock 類。但是,接口越大越復(fù)雜,就越難為每個(gè)單元測(cè)試編寫單獨(dú)的 mock。通過(guò)使用 EasyMock,只需一行代碼就能夠創(chuàng)建
java.sql.ResultSet
或
org.xml.sax.ContentHandler
這樣的大型接口的實(shí)現(xiàn),然后向它們提供運(yùn)行測(cè)試所需的行為。
?
![]() ![]() |
?
mock 最常見(jiàn)的用途之一是測(cè)試異常條件。例如,無(wú)法簡(jiǎn)便地根據(jù)需要制造網(wǎng)絡(luò)故障,但是可以創(chuàng)建模擬網(wǎng)絡(luò)故障的 mock。
當(dāng)
getRate()
拋出
IOException
時(shí),
Currency
類應(yīng)該返回
null
。清單 4 測(cè)試這一點(diǎn):
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()
方法在被調(diào)用時(shí)拋出指定的異常。
可以拋出您需要的任何類型的異常(已檢查、運(yùn)行時(shí)或錯(cuò)誤),只要方法簽名支持它即可。這對(duì)于測(cè)試極其少見(jiàn)的條件(例如內(nèi)存耗盡錯(cuò)誤或無(wú)法找到類定義)或表示虛擬機(jī) bug 的條件(比如 UTF-8 字符編碼不可用)尤其有幫助。
?
![]() ![]() |
?
EasyMock 不只是能夠用固定的結(jié)果響應(yīng)固定的輸入。它還可以檢查輸入是否符合預(yù)期。例如,假設(shè)
toEuros()
方法有一個(gè) bug(見(jiàn)清單 5),它返回以歐元為單位的結(jié)果,但是獲取的是加拿大元的匯率。這會(huì)讓客戶發(fā)一筆意外之財(cái)或遭受重大損失。
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)足夠了。
?
![]() ![]() |
?
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 文檔:
<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:
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); } } |
?
這個(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
傳遞文本的各種方式。
?
![]() ![]() |
?
有必要使用 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)。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
