測(cè)試驅(qū)動(dòng)開(kāi)發(fā)是軟件開(kāi)發(fā)的重要部分。如果代碼不進(jìn)行測(cè)試,是不可靠的。所有代碼都必須測(cè)試,而且理想情況下應(yīng)該在編寫(xiě)代碼之前編寫(xiě)測(cè)試。但是,有些東西容易測(cè)試,有些東西不容易。如果要編寫(xiě)一個(gè)代表貨幣值的簡(jiǎn)單的類(lèi),那么很容易測(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ù)查詢(xún)隨時(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í)候稱(chēng)為 fake。
mock 對(duì)象還有助于測(cè)試錯(cuò)誤條件。例如,如果 toEuros() 方法試圖獲取新的匯率,但是網(wǎng)絡(luò)中斷了,那么會(huì)發(fā)生什么?可以把以太網(wǎng)線(xiàn)從計(jì)算機(jī)上拔出來(lái),然后運(yùn)行測(cè)試,但是編寫(xiě)一個(gè)模擬網(wǎng)絡(luò)故障的 mock 對(duì)象省事得多。
mock 對(duì)象還可以測(cè)試類(lèi)的行為。通過(guò)把斷言放在 mock 代碼中,可以檢查要測(cè)試的代碼是否在適當(dāng)?shù)臅r(shí)候把適當(dāng)?shù)膮?shù)傳遞給它的協(xié)作者?梢酝ㄟ^(guò) mock 查看和測(cè)試類(lèi)的私有部分,而不需要通過(guò)不必要的公共方法公開(kāi)它們。
后,mock 對(duì)象有助于從測(cè)試中消除依賴(lài)項(xiàng)。它們使測(cè)試更單元化。涉及 mock 對(duì)象的測(cè)試中的失敗很可能是要測(cè)試的方法中的失敗,不太可能是依賴(lài)項(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 類(lèi)擴(kuò)展,還可以為類(lèi)創(chuàng)建 mock。可以針對(duì)任何用途配置這些 mock,從方法簽名中的簡(jiǎn)單啞參數(shù)到檢驗(yàn)一系列方法調(diào)用的多調(diào)用測(cè)試。
EasyMock 簡(jiǎn)介
現(xiàn)在通過(guò)一個(gè)具體示例演示 EasyMock 的工作方式。清單 1 是虛構(gòu)的 ExchangeRate 接口。與任何接口一樣,接口只說(shuō)明實(shí)例要做什么,而不指定應(yīng)該怎么做。例如,它并沒(méi)有指定從 Yahoo 金融服務(wù)、政府還是其他地方獲取匯率數(shù)據(jù)。
清單 1. ExchangeRate
import java.io.IOException;
public interface ExchangeRate {
double getRate(String inputCurrency, String outputCurrency) throws IOException;
}
清單 2 是假定的 Currency 類(lèi)的骨架。它實(shí)際上相當(dāng)復(fù)雜,很可能包含 bug。(您不必猜了:確實(shí)有 bug,實(shí)際上有不少)。
清單 2. Currency 類(lèi)
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 類(lèi)設(shè)計(jì)的一些重點(diǎn)可能不容易一下子看出來(lái)。匯率是從這個(gè)類(lèi)之外 傳遞進(jìn)來(lái)的,并不是在類(lèi)內(nèi)部構(gòu)造的。因此,很有必要為匯率創(chuàng)建 mock,這樣在運(yùn)行測(cè)試時(shí)不需要與真正的匯率服務(wù)器通信。這還使客戶(hù)機(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ì)象。
清單 3. CurrencyTest 類(lèi)
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);
}
}
老實(shí)說(shuō),在我第一次運(yùn)行 清單 3 時(shí)失敗了,測(cè)試中經(jīng)常出現(xiàn)這種問(wèn)題。但是,我已經(jīng)糾正了 bug。這是我們采用 TDD 的原因。
運(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)有編寫(xiě)實(shí)現(xiàn) ExchangeRate 接口的類(lèi)。另外,EasyMock.createMock() 方法無(wú)法返回 ExchangeRate 的實(shí)例,它根本不知道這個(gè)類(lèi)型,這個(gè)類(lèi)型是我為本文創(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)該重放什么。
如果在使用 mock 之前忘了調(diào)用 EasyMock.replay(),那么會(huì)出現(xiàn) IllegalStateException 異常和一個(gè)沒(méi)有什么幫助的錯(cuò)誤消息:missing behavior definition for the preceding method call。
接下來(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è)試的方法:
為類(lèi)創(chuàng)建 mock
從實(shí)現(xiàn)的角度來(lái)看,很難為類(lèi)創(chuàng)建 mock。不能為類(lèi)創(chuàng)建動(dòng)態(tài)代理。標(biāo)準(zhǔn)的 EasyMock 框架不支持類(lèi)的 mock。但是,EasyMock 類(lèi)擴(kuò)展使用字節(jié)碼操作產(chǎn)生相同的效果。您的代碼中采用的模式幾乎完全一樣。只需導(dǎo)入 org.easymock.classextension.EasyMock 而不是 org.easymock.EasyMock。為類(lèi)創(chuàng)建 mock 允許把類(lèi)中的一部分方法替換為 mock,而其他方法保持不變。
Currency actual = testObject.toEuros(mock);
后,檢查結(jié)果是否符合預(yù)期:
assertEquals(expected, actual);
這完成了。如果有一個(gè)需要返回特定值的接口需要測(cè)試,可以快速地創(chuàng)建一個(gè) mock。這確實(shí)很容易。ExchangeRate 接口很小很簡(jiǎn)單,很容易為它手工編寫(xiě) mock 類(lèi)。但是,接口越大越復(fù)雜,越難為每個(gè)單元測(cè)試編寫(xiě)單獨(dú)的 mock。通過(guò)使用 EasyMock,只需一行代碼能夠創(chuàng)建 java.sql.ResultSet 或 org.xml.sax.ContentHandler 這樣的大型接口的實(shí)現(xiàn),然后向它們提供運(yùn)行測(cè)試所需的行為。
測(cè)試異常
mock 常見(jiàn)的用途之一是測(cè)試異常條件。例如,無(wú)法簡(jiǎn)便地根據(jù)需要制造網(wǎng)絡(luò)故障,但是可以創(chuàng)建模擬網(wǎng)絡(luò)故障的 mock。
當(dāng) getRate() 拋出 IOException 時(shí),Currency 類(lèi)應(yīng)該返回 null。清單 4 測(cè)試這一點(diǎn):
清單 4. 測(cè)試方法是否拋出正確的異常
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í)拋出指定的異常。
可以?huà)伋瞿枰娜魏晤?lèi)型的異常(已檢查、運(yùn)行時(shí)或錯(cuò)誤),只要方法簽名支持它即可。這對(duì)于測(cè)試極其少見(jiàn)的條件(例如內(nèi)存耗盡錯(cuò)誤或無(wú)法找到類(lèi)定義)或表示虛擬機(jī) bug 的條件(比如 UTF-8 字符編碼不可用)尤其有幫助。