在我們單元測試的實踐中,常常會發現一個方法依賴一個無法控制的對象,我們稱其爲外部依賴項。
一個外部依賴項——是系統中的一個對象,被測試代碼與這個對象發生交互,但你不能控制這個對象。(常見的外部依賴項包括文件系統、線程、內存以及時間等。)
而單元測試背後的思想是,僅測試這個方法中的內容,當測試開始滲透到其他類、服務或系統時,此時測試便跨越了邊界。而一旦測試跨了邊界就變成了集成測試。進而也帶來了所有與集成測試相關的問題——運行速度較慢,需要配置,一次測試多個內容......
1 樁對象(存根)
什麼是樁對象(存根)
一個存根(樁對象)(stub)是對系統中存在的一個依賴項(或者協作者)的可控制的替代物。通過使用存根,你在測試代碼時無需直接處理這個依賴項。
如何使用樁對象(存根)破除依賴
示例1
假設我們有下面這樣一個方法,從文件系統中讀取一個文件,獲取文件的擴展名,如果擴展名是jpg就返回true,否則返回false。
IFileExtensionManager fileManager;
public bool IsValidFileName(){
//獲取文件擴展名
string extName=fileManager.GetExtName();
if(extName=="jpg"){
return true;
}
return false;
}
public class FileExtensionManager:IFileExtensionManager{
public string GetExtName(){
//調用真實的文件系統獲取文件
return file.GetExtension();
}
}
很明顯在這個方法中,我們要測的邏輯是擴展名是jpg就返回true,否則返回false。這個方法我們依賴一個外部方法FileExtensionManage.GetExtName()(獲取文件的擴展名)。
使用存根破除依賴一般有下面幾個步驟
- 找到被測試對象使用的外部接口
- 把這個接口的底層實現替換成你能控制的東西。
IFileExtensionManager fileManager;
public bool IsValidFileName(){
//獲取文件擴展名
string extName=fileManager.GetExtName();
if(extName=="jpg"){
return true;
}
return false;
}
public class StubFileExtensionManager:IFileExtensionManager{
public string GetExtName(){
// 模擬文件系統的返回結果
return "jpg";
}
}
我們所創造的替代實例StubFileExtensionManager根本不會訪問文件系統,這樣就破除了對文件系統的依賴性。因爲要測試的不是訪問文件系統的類,而是調用這個類的代碼,這個時候我們的的依賴關係就變成了下面這樣
示例2
在上面的示例中,我們的被測試類與文件系統幫助類並非是強依賴的,而是依賴倒置的(通過接口IFileExtensionManager解耦),而在有些系統中,對於文件系統的訪問類可能是下面這樣的
public bool IsValidFileName(){
//獲取文件擴展名
string extName=new FileExtensionManager().GetExtName();
if(extName=="jpg"){
return true;
}
return false;
}
這種情況下由於代碼的不可測試性,我們就需要先對代碼進行重構。使其更具有可測試性(注意:可測試性同樣是我們編碼所需要注意的原則之一)
- 找到被測試的工作單元依賴的外部對象。
- 如果這個外部對象與被測試工作單元直接相連(本例中,你直接讀取文件系統),就在代碼中添加一個間接層。
- 把這個交互接口的底層實現替換成你可以控制的代碼。
此時變成了示例1的情況,就可以進行測試。
而在實踐過程中,我們還會遇到許多難以測試的代碼,這時就需要通過重構來提高其可測試性。關於如何是代碼變得更加容易測試,後續文章繼續總結。
2 模擬對象
什麼是模擬對象
模擬對象可以驗證被測試對象是否接預期的方式調用了這個僞對象,因此導致單元測試通過或是失敗。
模擬對象主要用來做交互性測試,例如:調用一個第三方日誌系統,你所調用的方法並不會返回任何東西,我們如何判斷是否調用正確,甚至是否發生了調用。
如何利用模擬對象進行交互測試
如下示例,在我們的業務方法中如果文件名的長度大於8就要記錄一個warn日誌。這個方法不返回任何值,其所調用的日誌系統的方法也不返回任何值。這個時候我們要驗證是否如期調用了日誌系統的warn方法。
public class FlieService{
ILogger logger;
public FlieService(ILogger logger){
this.logger=logger;
}
// 被測方法
public void LogValidResult(string fileName){
if(fileName.length>8){
logger.warn("invalid ...",obj);
}
}
}
//測試方法
[Test]
public void LogValidResult_Valid_Logger(){
string fileName="hello world"
var logger=new MockLogger();
new FileService(logger).LogValidResult();
string expect="invalid ...";
string actual=logger.Title;
Assert.AreEqual(expect,actual);
}
// 模擬對象
public class MockLogger:ILogger{
public string Title{get;set;}
public void info(string title,object obj){
}
}
3 僞對象、模擬對象與樁對象
僞對象
僞對象是通用的術語,可以描述一個存根或者模擬對象(手工或非手工編寫),因爲存根和模擬對象看上去都很像真實對象。一個僞對象究竟是存根還是模擬對象取決於它在當前測試中的使用方式:如果這個僞對象用來檢驗一個交互(對其進行斷言),它就是模擬對象,否則就是存根
模擬對象與樁對象的區別
乍一看模擬對象與樁對象很相似,或者根本不存在區別。但區分二者又很重要,因爲會使用這兩個詞來描述框架的各種不同行爲。
二者最根本的區別是存根不會導致測試失敗,而模擬對象可以
要辨別你是否使用了存根,最簡單的方法是:存根永遠不會導致測試失敗。測試總是對被測試類進行斷言
另一方面,測試會使用模擬對象驗證測試是否失敗。下圖展示了測試和模擬對象之前的交互。
4 小結
本文簡單總結了,當單元測試遇到外部依賴對象的時候我們通過樁對象來破除依賴,而在涉及驗證是否正確調用一個外部對象的時候,我們可以使用模擬對象來進行交互測試。
可以看到這裏我們用來創造僞對象都是通過自己手寫代碼的方式,而真實項目中有時候可能需要多個僞對象,那麼又有什麼好的方式呢。實際上現在無論是.net和java爲了更好的單測已經產生了許多好用的單測框架與模擬框架。弄明白單測的一些基本思想,再熟練的運用好這些框架,將會讓我們的單元測試進行的更加如魚得水。