前言
在上一篇文章中,提到了如何通過 IoC 的設計,以及 Stub Object 的方式,來獨立測試目標物件。
這一篇文章,則要說明有哪些設計物件的方式,可以讓測試或需求變更時,更容易轉換。
並說明這些方式有哪些特性,供讀者朋友們在設計時,可以選擇適合自己情境的方式來使用。
需求說明
當呼叫目標物件的方法時,期望目標物件的內容可以不必關注相依於哪些實體物件,而只需要依賴於某個介面,通過這樣的方式來達到設計的彈性與可獨立測試性。
那麼,有哪一些方式可以達到這樣的目的呢?
建構函式(constructor)
描述:
上一篇文章範例所使用的方式,將物件的相依介面,拉到公開的建構函式,供外部物件使用時,可自行組合目標物件的依賴物件實體。
public class Validation
{
private IAccountDao _accountDao;
private IHash _hash;
public Validation(IAccountDao dao, IHash hash)
{
this._accountDao = dao;
this._hash = hash;
}
public bool CheckAuthentication(string id, string password)
{
var passwordByDao = this._accountDao.GetPassword(id);
var hashResult = this._hash.GetHashResult(password);
return passwordByDao == hashResult;
}
}
好處:
有許多 DI framework 支援 Autowiring。
Autowiring is an automatic detection of dependency injection points.
這裡的 dependency injection points 在這例子,指的就是建構函式。以 Unity 為例,在 UnityContainer 取得目標物件時,會自動尋找目標物件引數最多的建構函式。並針對每一個引數的型別,繼續在 UnityContainer 中尋找對應的實體物件,直到目標物件組合完畢,回傳一個完整的目標物件。
由建構函式傳入依賴介面的實體物件,是一個很通用的方式。因此在結合許多常見的 DI framework,不需要再額外處理。
顧慮點:
當物件越來越複雜時,建構函式也會趨於複雜。倘若沒有 DI framework 的輔助,則使用物件上,面對許多 overload 的建構函式,或是一個建構函式的引數有好幾個,會造成使用目標物件上的困難與疑惑。若沒有好好進行 refactoring,也可能因此而埋藏許多 bad smell。
另外,倘若是許多建構函式,也可能造成要呼叫 A 方法時,應選用 A 對應的建構函式,但在使用物件上,可能會用錯建構函式而不自知,若方法中沒有正確的防呆,則可能出現錯誤。(請搭配單元測試的測試案例來輔助)
最後,與原本直接依賴的程式程式碼相比較,目標物件的相依物件因此暴露出來,交由外部決定,而喪失了一點封裝的意味。而使用端也不一定知道,要取用此物件時,應該要注入哪些相依物件。(請使用 Repository Pattern 或 DI framework 來輔助)
公開屬性(public setter property)
描述:
其實公開屬性與公開建構函式非常類似,通過 public 的 property(property 型別仍為 interface),讓外部在使用目標物件時,可先 setting 目標物件的相依物件,接著才呼叫其方法。
而公開屬性通常只會將 setter 公開給外部設定,getter 則設定為 private。原因很簡單,外部只需設定,而不需取用。就像公開建構函式,在使用物件之前先傳入初始化物件必備的資訊,但目標物件可能將這些資訊,存放在 private 的 filed 或 property 中,而不需再提供給外部使用。
程式程式碼如下:
public class Validation
{
public bool CheckAuthentication(string id, string password)
{
var accountDao = GetAccountDao();
var passwordByDao = accountDao.GetPassword(id);
var hash = GetHash();
var hashResult = hash.GetHashResult(password);
return passwordByDao == hashResult;
}
private Hash GetHash()
{
var hash = new Hash();
return hash;
}
private AccountDao GetAccountDao()
{
var accountDao = new AccountDao();
return accountDao;
}
}
沒什麼改變,對吧?
接下來,將兩個 new 物件的方法,宣告為 protected virtual
,代表子類別可以繼承與重寫該方法。程式程式碼如下:
protected virtual Hash GetHash()
{
var hash = new Hash();
return hash;
}
protected virtual AccountDao GetAccountDao()
{
var accountDao = new AccountDao();
return accountDao;
}
另外,將要使用到 Hash 與 AccountDao 的方法,也要宣告為 virtual
。程式程式碼如下:
public class AccountDao
{
public virtual string GetPassword(string id)
{
throw new NotImplementedException();
}
}
public class Hash
{
public virtual string GetHashResult(string password)
{
throw new NotImplementedException();
}
}
到這裡,都不影響外部使用目標物件的行為,我們只是在重構物件的內部方法罷了。事實上,我們可測試性的動作也準備完畢了。(當然,建議還是要依賴於介面,實現介面要顧慮的點,比繼承類要輕鬆的多)
接下來把目光切到測試程式,該如何對 CheckAuthentication 方法進行測試。
首先,將上一篇文章的 StubHash 改為繼承自 Hash,StubAccountDao 改為繼承自 AccountDao,並將原本 public 的方法,加上 override
關鍵詞,重寫其父類方法內容。程式程式碼如下:
public class StubAccountDao : AccountDao
{
public override string GetPassword(string id)
{
return "Hello World";
}
}
public class StubHash : Hash
{
public override string GetHashResult(string password)
{
return "Hello World";
}
}
不難,對吧。接下來,建立一個 MyValidation 的 class,繼承自 Validation。並重寫 GetAccountDao() 與 GetHash(),使其回傳 Stub Object。程式程式碼如下:
public class MyValidation : Validation
{
protected override AccountDao GetAccountDao()
{
return new StubAccountDao();
}
protected override Hash GetHash()
{
return new StubHash();
}
}
也不難,對吧。接下來,來設計單元測試,程式程式碼如下:
[TestMethod()]
public void CheckAuthenticationTest()
{
Validation target = new MyValidation();
string id = "id隨便";
string password = "密碼也隨便";
bool expected = true;
bool actual;
actual = target.CheckAuthentication(id, password);
Assert.AreEqual(expected, actual);
}
原本初始化的測試目標為 Validation 物件,現在則為 MyValidation 物件。裡面唯一不同的部分,只有重寫的方法內容,其餘 MyValidation 就等同於 Validation。(Is-A的關係)除錯測試一下,就可以確認,程式程式碼就跟之前使用 IoC 的方式執行沒有太大的差異。
好處:
這個方式最大的好處,是完全不影響外部使用物件的方式。僅透過 protected 與 virtual 來對繼承鏈開放擴充的功能,並且透過這樣的方式,就使得原本直接相依而導致無法測試的問題,獲得解套。
顧慮點:
這是為了測試,且面對 legacy code 所使用的方式,而不是良好的物件導向設計的方式。IoC 的用意在於面向藉口與擴充點的彈性,所以當可測試之後,倘若重構影響範圍不大,建議讀者朋友還是要將物件改依賴於介面,通過IoC 的方式來設計物件。
by the way, 同樣為了解決直接相依物件,甚至相依於 static 方法、.net framework 本身的物件(如 DateTime.Now)而導致無法測試的問題,還有另外一個方式,稱為 fake object。這在後面的文章,會再進行較為詳盡的介紹。
結論
以上幾種用來測試的方式,希望對各位讀者在不同情境下的設計,可以有所幫助。
而許多延伸的議題,在這系列文章並不會多談,但在實務應用面上,卻是相當重要的配套措施。例如一再提到的 DI framework, Repository Pattern,以及通過測試程式來說明物件的使用方式,請讀者在現實設計系統時,務必瞭解這些東西如何讓系統設計更加完整。
下一篇文章,將介紹怎麼樣可以避免每次手工敲打這麼囉唆的 stub 物件,怎麼針對 static 或 .net framework 本身的物件進行隔離,怎麼針對物件與相依介面互動的情況進行測試。
備註:這個系列是我畢業後時隔一年重新開始進入開發行業後對大拿們的博文摘要整理進行學習對自我的各個欠缺的方面進行充電記錄部落格的過程,非原創,特此感謝91 等前輩