在測試中,如果因?yàn)榇a對外部資源存在依賴的行為,盡管代碼的邏輯是正確的也會(huì)存在測試失敗的可能性,這也被稱為test-inhibiting
常見的external dependency是系統(tǒng)中的一個(gè)對象,被測試的代碼需要與這個(gè)對象進(jìn)行交互,但是你并不能去控制這個(gè)對象的行為。
而stub則是對于系統(tǒng)的external dependency的可控制的替代物。stub帶來的效果就是可以在測試代碼中無需對external dependency進(jìn)行直接處理。
提高代碼的可測試性
破除dependency最直接的方式就是引入seam,當(dāng)然這比如是需要refactoring配合的
A型方法:把具體類抽象成接口
抽取接口以便對實(shí)現(xiàn)進(jìn)行替換
B型方法:注入委托和接口(fake implementation)
在被測試類注入stub
在構(gòu)造函數(shù)注入偽對象
利用屬性注入的方式注入偽對象
在方法調(diào)用前注入問對象
抽取接口方式
public class ExternalManager:IExternalManager
{
public bool IsExternal (string name)
{
......
}
}
public interface IExternalManager
{
bool IsExternal(string name);
}
//測試單元
public bool IsExternalManager(string name)
{
IexternalManager mgr = new ExternalManager();
return mgr.IsexternalManager(name);
}
返回值為true的stub
public class AlwaysTrueExternalManager:IExternalManager
{
public bool IsExternal (string name)
{
return true;
}
}
在構(gòu)造函數(shù)中注入偽對象
這種方式需要給測試類添加新的構(gòu)造函數(shù)或者是個(gè)其構(gòu)造函數(shù)添加新的參數(shù),傳入抽取出來的接口類型的對象,通過field或var 供被測試方法或其它相關(guān)區(qū)域的調(diào)用。
通過這種方式,測試需要先進(jìn)行stub的配置,然后傳入被測試對象。這種方式可以提高測試代碼的可讀性,將需要被了解的信息集中在一點(diǎn)地方。
同時(shí),利用構(gòu)造函數(shù)添加參數(shù)的方式實(shí)現(xiàn)注入,實(shí)際上也使得這些參數(shù)稱為不可選的依賴項(xiàng),也就是說類的使用者需要為每個(gè)所需的特定依賴傳入?yún)?shù)。
利用構(gòu)造函數(shù)注入偽對象也可能會(huì)帶來一些問題,比如擁有多個(gè)external dependency 。這時(shí)候難道要添加多個(gè)構(gòu)造函數(shù)或是添加多個(gè)參數(shù)?(明顯是不明智的選擇)
這個(gè)問題可以通過創(chuàng)建特殊的類,擁有可以初始化一個(gè)類所需要的所有值,這樣構(gòu)造函數(shù)就只需要擁有這個(gè)類作為唯一參數(shù),當(dāng)然更好的方式是通過IoC去實(shí)現(xiàn)。
屬性注入
這個(gè)case需要為每個(gè)要注入的dependency添加一個(gè)屬性,包括get和set。然后只需要在被測函數(shù)中相應(yīng)的地方使用這些依賴。 與構(gòu)造函數(shù)的依賴注入方式一樣,需要指明必需的和可選的依賴項(xiàng),因此也會(huì)影響到api的設(shè)計(jì)。通過使用屬性,也同時(shí)說明了使用這個(gè)函數(shù),那些依賴項(xiàng)是不需要的(可能這也是需要使用的屬性注入的場景之一)。
方法調(diào)用前注入偽對象
這個(gè)case很明顯的的意思就是我在對這個(gè)對象進(jìn)行操作之前才可以得到其實(shí)例,而不是在構(gòu)造函數(shù)或?qū)傩灾芯鸵呀?jīng)準(zhǔn)備好,這個(gè)case的不同之處就是發(fā)起stub請求的對象就是被測試代碼。
這樣的話,可以通過使用工廠類來作為實(shí)例的提供者,當(dāng)然必須在構(gòu)造函數(shù)中初始化這個(gè)實(shí)例提供者。在這個(gè)工廠中,可以使用靜態(tài)方法返回實(shí)例了接口的對象實(shí)例,同時(shí)采用別的方式(如擴(kuò)展名)返回stub(這種方法會(huì)破壞類的設(shè)計(jì)封裝)。
返回stub的層次
第一層:在被測試類中偽造一個(gè)成員
添加構(gòu)造函數(shù),在構(gòu)造函數(shù)中設(shè)置類,從測試代碼中設(shè)置構(gòu)造參數(shù),還要考慮對api的影響。
這種方式會(huì)改變被測試類的語義,在沒有充分理由情況下一般不采用這種方式。
第二層:在工廠類偽造一個(gè)成員
上述方法調(diào)用前注入偽對象中提到的就是這一層,把工廠類的屬性設(shè)置成偽依賴項(xiàng),這種方式并不會(huì)改變語義,一切都是原樣,代碼也較為簡潔。但是使用這種方式,必須要充分了解實(shí)例的使用時(shí)間。
第三層:偽造工廠類
這種方式需要實(shí)現(xiàn)自己專有的偽工廠類,同時(shí)這個(gè)類也能有或者可能沒有接口,若沒有,則需要為這個(gè)工廠類創(chuàng)建接口,然后再創(chuàng)建偽工廠實(shí)例,讓其返回依賴項(xiàng)。這種層次可以理解為利用一個(gè)偽對象去返回一個(gè)偽對象,這個(gè)實(shí)在是很難想象的事情。
偽造方法
這種方法的層次,不同于上述幾種,相比而言,其更接近于被測試代碼(一般而言,約接近代碼,越少需要更改依賴項(xiàng))。很重要的一點(diǎn)就是這種方式將被測試的代碼也作為一個(gè)依賴項(xiàng)。
使用方式:
在測試類中:
添加返回真是實(shí)例的虛工廠方法
在代碼中正常的使用工廠方法
在測試項(xiàng)目中:
創(chuàng)建新的類
聲明這個(gè)新類繼承被測試類
創(chuàng)建需要替換的接口類型的公共字段(field)
重寫虛工廠方法
返回公共字段
測試代碼中
創(chuàng)建一個(gè)stub類的實(shí)例(實(shí)現(xiàn)相應(yīng)接口)
創(chuàng)建新的派生類而非被測試類的實(shí)例
配置新實(shí)例的公共字段(設(shè)置成stub實(shí)例)
在測試的派生類中,通過重寫工廠方法,產(chǎn)品代碼將使用配置的偽對象
這種抽取和重寫的方式,可以直接替換依賴項(xiàng),實(shí)現(xiàn)方法避免了大量和接口和虛函數(shù)。
這種方式比較適合用于模擬提供給被測試代碼的輸入,但是不適合驗(yàn)證被測試代碼到依賴項(xiàng)的調(diào)用。 如果被測試代碼是web服務(wù)時(shí),得到一個(gè)返回值,這是如果想要模擬自己的返回值的話,這種方法就很適合。但是如果想要測試代碼對于web服務(wù)的調(diào)用時(shí)是否正確就顯得捉襟見肘。 當(dāng)代碼中以及存在可以偽造的接口的時(shí)候,有明顯的可以使用seam的位置,便不需要使用這種方式。當(dāng)然如果面對一個(gè)密封類,這種方法也顯得很無力,無從下手。。。。。