偽物件、樁物件、模擬物件|單元測試

老於`發表於2021-01-25

在我們單元測試的實踐中,常常會發現一個方法依賴一個無法控制的物件,我們稱其為外部依賴項。
一個外部依賴項——是系統中的一個物件,被測試程式碼與這個物件發生互動,但你不能控制這個物件。(常見的外部依賴項包括檔案系統、執行緒、記憶體以及時間等。)
而單元測試背後的思想是,僅測試這個方法中的內容,當測試開始滲透到其他類、服務或系統時,此時測試便跨越了邊界。而一旦測試跨了邊界就變成了整合測試。進而也帶來了所有與整合測試相關的問題——執行速度較慢,需要配置,一次測試多個內容......

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()(獲取檔案的副檔名)。image.png
使用存根破除依賴一般有下面幾個步驟

  1. 找到被測試物件使用的外部介面
  2. 把這個介面的底層實現替換成你能控制的東西。
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根本不會訪問檔案系統,這樣就破除了對檔案系統的依賴性。因為要測試的不是訪問檔案系統的類,而是呼叫這個類的程式碼,這個時候我們的的依賴關係就變成了下面這樣

image.png

示例2

在上面的示例中,我們的被測試類與檔案系統幫助類並非是強依賴的,而是依賴倒置的(通過介面IFileExtensionManager解耦),而在有些系統中,對於檔案系統的訪問類可能是下面這樣的

public bool IsValidFileName(){
    //獲取副檔名
    string extName=new FileExtensionManager().GetExtName();
    if(extName=="jpg"){
    	return true;
    }
    return false;
}

image.png

這種情況下由於程式碼的不可測試性,我們就需要先對程式碼進行重構。使其更具有可測試性(注意:可測試性同樣是我們編碼所需要注意的原則之一)

  1. 找到被測試的工作單元依賴的外部物件。
  2. 如果這個外部物件與被測試工作單元直接相連(本例中,你直接讀取檔案系統),就在程式碼中新增一個間接層。
  3. 把這個互動介面的底層實現替換成你可以控制的程式碼。

image.png

此時變成了示例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){
    	
    }
}

image.png

3 偽物件、模擬物件與樁物件

偽物件

偽物件是通用的術語,可以描述一個存根或者模擬物件(手工或非手工編寫),因為存根和模擬物件看上去都很像真實物件。一個偽物件究竟是存根還是模擬物件取決於它在當前測試中的使用方式:如果這個偽物件用來檢驗一個互動(對其進行斷言),它就是模擬物件,否則就是存根

模擬物件與樁物件的區別

乍一看模擬物件與樁物件很相似,或者根本不存在區別。但區分二者又很重要,因為會使用這兩個詞來描述框架的各種不同行為。
二者最根本的區別是存根不會導致測試失敗,而模擬物件可以
要辨別你是否使用了存根,最簡單的方法是:存根永遠不會導致測試失敗。測試總是對被測試類進行斷言
另一方面,測試會使用模擬物件驗證測試是否失敗。下圖展示了測試和模擬物件之前的互動。

image.png

4 小結

本文簡單總結了,當單元測試遇到外部依賴物件的時候我們通過樁物件來破除依賴,而在涉及驗證是否正確呼叫一個外部物件的時候,我們可以使用模擬物件來進行互動測試。
可以看到這裡我們用來創造偽物件都是通過自己手寫程式碼的方式,而真實專案中有時候可能需要多個偽物件,那麼又有什麼好的方式呢。實際上現在無論是.net和java為了更好的單測已經產生了許多好用的單測框架與模擬框架。弄明白單測的一些基本思想,再熟練的運用好這些框架,將會讓我們的單元測試進行的更加如魚得水。

相關文章