測試驅(qū)動開發(fā)是軟件開發(fā)的重要部分。如果代碼不進(jìn)行測試,是不可靠的。所有代碼都必須測試,而且理想情況下應(yīng)該在編寫代碼之前編寫測試。但是,有些東西容易測試,有些東西不容易。如果要編寫一個(gè)代表貨幣值的簡單的類,那么很容易測試把 $1.23 和 $2.8 相加是否能夠得出 $4.03,而不是 $3.03 或 $4.029999998。測試是否不會出現(xiàn) $7.465 這樣的貨幣值也不太困難。但是,如何測試把 $7.50 轉(zhuǎn)換為 €5.88 的方法呢(尤其是在通過連接數(shù)據(jù)庫查詢隨時(shí)變動的匯率信息的情況下)?在每次運(yùn)行程序時(shí),amount.toEuros() 的正確結(jié)果都可能有變化。
答案是 mock 對象。測試并不通過連接真正的服務(wù)器來獲取新的匯率信息,而是連接一個(gè) mock 服務(wù)器,它總是返回相同的匯率。這樣可以得到可預(yù)測的結(jié)果,可以根據(jù)它進(jìn)行測試。畢竟,測試的目標(biāo)是 toEuros() 方法中的邏輯,而不是服務(wù)器是否發(fā)送正確的值。(那是構(gòu)建服務(wù)器的開發(fā)人員要操心的事)。這種 mock 對象有時(shí)候稱為 fake。
mock 對象還有助于測試錯(cuò)誤條件。例如,如果 toEuros() 方法試圖獲取新的匯率,但是網(wǎng)絡(luò)中斷了,那么會發(fā)生什么?可以把以太網(wǎng)線從計(jì)算機(jī)上拔出來,然后運(yùn)行測試,但是編寫一個(gè)模擬網(wǎng)絡(luò)故障的 mock 對象省事得多。
mock 對象還可以測試類的行為。通過把斷言放在 mock 代碼中,可以檢查要測試的代碼是否在適當(dāng)?shù)臅r(shí)候把適當(dāng)?shù)膮?shù)傳遞給它的協(xié)作者。可以通過 mock 查看和測試類的私有部分,而不需要通過不必要的公共方法公開它們。
后,mock 對象有助于從測試中消除依賴項(xiàng)。它們使測試更單元化。涉及 mock 對象的測試中的失敗很可能是要測試的方法中的失敗,不太可能是依賴項(xiàng)中的問題。這有助于隔離問題和簡化調(diào)試。
EasyMock 是一個(gè)針對 Java 編程語言的開放源碼 mock 對象庫,可以幫助您快速輕松地創(chuàng)建用于這些用途的 mock 對象。EasyMock 使用動態(tài)代理,讓您只用一行代碼能夠創(chuàng)建任何接口的基本實(shí)現(xiàn)。通過添加 EasyMock 類擴(kuò)展,還可以為類創(chuàng)建 mock?梢葬槍θ魏斡猛九渲眠@些 mock,從方法簽名中的簡單啞參數(shù)到檢驗(yàn)一系列方法調(diào)用的多調(diào)用測試。
EasyMock 簡介
現(xiàn)在通過一個(gè)具體示例演示 EasyMock 的工作方式。清單 1 是虛構(gòu)的 ExchangeRate 接口。與任何接口一樣,接口只說明實(shí)例要做什么,而不指定應(yīng)該怎么做。例如,它并沒有指定從 Yahoo 金融服務(wù)、政府還是其他地方獲取匯率數(shù)據(jù)。
清單 1. ExchangeRate
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í)際上有不少)。
清單 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 類設(shè)計(jì)的一些重點(diǎn)可能不容易一下子看出來。匯率是從這個(gè)類之外 傳遞進(jìn)來的,并不是在類內(nèi)部構(gòu)造的。因此,很有必要為匯率創(chuàng)建 mock,這樣在運(yùn)行測試時(shí)不需要與真正的匯率服務(wù)器通信。這還使客戶機(jī)應(yīng)用程序能夠使用不同的匯率數(shù)據(jù)源。
清單 3 給出一個(gè) JUnit 測試,它檢查在匯率為 1.5 的情況下 $2.50 是否會轉(zhuǎn)換為 €3.75。使用 EasyMock 創(chuàng)建一個(gè)總是提供值 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);
}
}
老實(shí)說,在我第一次運(yùn)行 清單 3 時(shí)失敗了,測試中經(jīng)常出現(xiàn)這種問題。但是,我已經(jīng)糾正了 bug。這是我們采用 TDD 的原因。
運(yùn)行這個(gè)測試,它通過了。發(fā)生了什么?我們來逐行看看這個(gè)測試。首先,構(gòu)造測試對象和預(yù)期的結(jié)果:
Currency testObject = new Currency(2.50, "USD");
Currency expected = new Currency(3.75, "EUR");
這不是新東西。
接下來,通過把 ExchangeRate 接口的 Class 對象傳遞給靜態(tài)的 EasyMock.createMock() 方法,創(chuàng)建這個(gè)接口的 mock 版本:
ExchangeRate mock = EasyMock.createMock(ExchangeRate.class);
這是到目前為止不可思議的部分。注意,我可沒有編寫實(shí)現(xiàn) ExchangeRate 接口的類。另外,EasyMock.createMock() 方法無法返回 ExchangeRate 的實(shí)例,它根本不知道這個(gè)類型,這個(gè)類型是我為本文創(chuàng)建的。即使它能夠通過某種奇跡返回 ExchangeRate,但是如果需要模擬另一個(gè)接口的實(shí)例,又會怎么樣呢?
我初看到這個(gè)時(shí)也非常困惑。我不相信這段代碼能夠編譯,但是它確實(shí)可以。這里的 “黑魔法” 來自 Java 1.3 中引入的 Java 5 泛型和動態(tài)代理(見 參考資料)。幸運(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(),那么會出現(xiàn) IllegalStateException 異常和一個(gè)沒有什么幫助的錯(cuò)誤消息:missing behavior definition for the preceding method call。
接下來,通過調(diào)用 EasyMock.replay() 方法,讓 mock 準(zhǔn)備重放記錄的數(shù)據(jù):
EasyMock.replay(mock);
這是讓我比較困惑的設(shè)計(jì)之一。EasyMock.replay() 不會實(shí)際重放 mock。而是重新設(shè)置 mock,在下一次調(diào)用它的方法時(shí),它將開始重放。
現(xiàn)在 mock 準(zhǔn)備好了,我把它作為參數(shù)傳遞給要測試的方法:
為類創(chuàng)建 mock
從實(shí)現(xiàn)的角度來看,很難為類創(chuàng)建 mock。不能為類創(chuàng)建動態(tài)代理。標(biāo)準(zhǔn)的 EasyMock 框架不支持類的 mock。但是,EasyMock 類擴(kuò)展使用字節(jié)碼操作產(chǎn)生相同的效果。您的代碼中采用的模式幾乎完全一樣。只需導(dǎo)入 org.easymock.classextension.EasyMock 而不是 org.easymock.EasyMock。為類創(chuàng)建 mock 允許把類中的一部分方法替換為 mock,而其他方法保持不變。
Currency actual = testObject.toEuros(mock);
后,檢查結(jié)果是否符合預(yù)期:
assertEquals(expected, actual);
這完成了。如果有一個(gè)需要返回特定值的接口需要測試,可以快速地創(chuàng)建一個(gè) mock。這確實(shí)很容易。ExchangeRate 接口很小很簡單,很容易為它手工編寫 mock 類。但是,接口越大越復(fù)雜,越難為每個(gè)單元測試編寫單獨(dú)的 mock。通過使用 EasyMock,只需一行代碼能夠創(chuàng)建 java.sql.ResultSet 或 org.xml.sax.ContentHandler 這樣的大型接口的實(shí)現(xiàn),然后向它們提供運(yùn)行測試所需的行為。
測試異常
mock 常見的用途之一是測試異常條件。例如,無法簡便地根據(jù)需要制造網(wǎng)絡(luò)故障,但是可以創(chuàng)建模擬網(wǎng)絡(luò)故障的 mock。
當(dāng) getRate() 拋出 IOException 時(shí),Currency 類應(yīng)該返回 null。清單 4 測試這一點(diǎn):
清單 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() 方法在被調(diào)用時(shí)拋出指定的異常。
可以拋出您需要的任何類型的異常(已檢查、運(yùn)行時(shí)或錯(cuò)誤),只要方法簽名支持它即可。這對于測試極其少見的條件(例如內(nèi)存耗盡錯(cuò)誤或無法找到類定義)或表示虛擬機(jī) bug 的條件(比如 UTF-8 字符編碼不可用)尤其有幫助。