看看下面的圖:
|-----------------------------------------------------------------|
| |
| |---------------------| | |-----------------------------|
| | Tested | <------------------------------------------à | External Mock Object |
| | Object | | |----------------------------|
| |---------------------| |
| /|\ |--------------------| |
| |-------〉 | Internal Mock | |
| | Object | |
| |--------------------| |
| [Your system scope] |
|--------------------------------------------------------- --------|
在測試你的Tested Object時,你可能會與你系統內的某個模塊或系統外某個實體交互,而這些模塊或實體在你做單元測試的時候可能并不存在,這時:
Ø Internal Mock Object可能是一個你的系統尚未完成的模塊的“替身”(replacement);
Ø External Mock Object可能是測試你的Tested Object時需要的外部的環境實體的“替身”(replacement)。
不知道這樣給Mock Object分類是否正確J
我們來看看與Real world object交互有什么不足之處:
Ø Real world object的行為具有不確定性,我們難于控制它們的輸出or返回結果。
Ø Real world object有些時候是難于被建立的或者說是無法獲得的。
Ø Real world object的有些行為難于被觸發,如磁盤已滿,網絡error等。
Ø Real world object可能不存在,比如你的Tested Object需要與你的系統的另一個module交互,而另一個module尚未開發完畢。
當然還不止這些,我們僅僅是列出一部分。
使用Mock Object替代Real world object后我們就會解決上述問題,換句話說當上面的情況出現后,我們就可以使用Mock Object。這也是什么時候該使用Mock Object的answer。
Mock Object是我們自己編寫的,我們擁有控制它的絕對的權力,我們可以定制它的行為和輸出。
Use Mock Object
使用Mock Object解決上述問題可分三步走:
1. Use an interface to describe the object
2. Implement the interface for production code
3. Implement the interface in a mock object for testing [3]
還有一點就是對于Internal Mock Object早晚你要實現出其Real world object的,因為那是你系統的一部分。
一個改自資料[3]的例子
public interface Environmental {
public long getTime();
// Other methods omitted...
}
對于這樣一個接口,我們提供兩種實現,
//real world object
public class SystemEnvironment implements Environmental {
public long getTime() {
return System.currentTimeMillis();
}
// other methods ...
}
//mock object
public class MockSystemEnvironment implements Environmental {
public long getTime() {
return currentTime;
}
public void setTime(Time aTime){
this.currentTime = aTime;
}
private Time currentTime;
//others
}
我們可以看到在MockSystemEnvironment中我們提供“setTime”函數是為了提供控制Mock Object的接口。
我們要測試的類
//TestedObject
public class TestedObject{
private Environmental env;
TestedObject(Environmental aEnv){
this.env = aEnv;
}
public boolean isAm(){
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(env.getTime());
int hour = cal.get(Calendar.HOUR_OF_DAY);
if (hour <=12) return true;
return false;
}
}
將要測試的類放入單元測試框架
public class TestTestedObject extends TestCase {
public void testIsAm(){
MockSystemEnvironment env = new MockSystemEnvironment();
// Set up a target test time
Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, 2004);
cal.set(Calendar.MONTH, 10);
cal.set(Calendar.DAY_OF_MONTH, 1);
cal.set(Calendar.HOUR_OF_DAY, 16);
cal.set(Calendar.MINUTE, 55);
long t1 = cal.getTimeInMillis();
env.setTime(t1);
TestedObject to = new TestedObject(env);
assertFalse(to.isAm());
}
}
在該單元測試中我們使用了Mock Object,并且在使用前我們利用setTime接口,輸入了我們需要的值。結果我們會通過測試。如果我們使用Real Object,我們得到的測試結果將是不固定的,后者可不是所期望的。從這個例子中你也應該體會到Mock object的一些好處了。
如果我們總是手動寫我們需要的Mock Object,那將是一個很大的工作量。現在業界有了Mock Objects、easy mock等開源框架的支持,是我們編寫Mock object變得越來越容易。
參考資料:
1、《Test-Driven Development – A practical guide》
2、《JUnit in action》
3、《Pragmatic Unit Testing》