前言
在上一篇文章中,提到了如何通過 IoC 的設(shè)計,以及 Stub Object 的方式,來獨立測試目標對象。
這一篇文章,則要說明有哪些設(shè)計對象的方式,可以讓測試或需求變更時,更容易轉(zhuǎn)換。
并說明這些方式有哪些特性,供讀者朋友們在設(shè)計時,可以選擇適合自己情境的方式來使用。
需求說明
當(dāng)調(diào)用目標對象的方法時,期望目標對象的內(nèi)容可以不必關(guān)注相依于哪些實體對象,而只需要依賴于某個接口,通過這樣的方式來達到設(shè)計的彈性與可獨立測試性。
那么,有哪一些方式可以達到這樣的目的呢?
構(gòu)造函數(shù)(constructor)
描述:
上一篇文章范例所使用的方式,將對象的相依接口,拉到公開的構(gòu)造函數(shù),供外部對象使用時,可自行組合目標對象的依賴對象實體。
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 在這例子,指的就是構(gòu)造函數(shù)。以 Unity 為例,在 UnityContainer 取得目標對象時,會自動尋找目標對象參數(shù)最多的構(gòu)造函數(shù)。并針對每一個參數(shù)的類型,繼續(xù)在 UnityContainer 中尋找對應(yīng)的實體對象,直到目標對象組合完畢,回傳一個完整的目標對象。
由構(gòu)造函數(shù)傳入依賴接口的實體對象,是一個很通用的方式。因此在結(jié)合許多常見的 DI framework,不需要再額外處理。
顧慮點:
當(dāng)對象越來越復(fù)雜時,構(gòu)造函數(shù)也會趨于復(fù)雜。倘若沒有 DI framework 的輔助,則使用對象上,面對許多 overload 的構(gòu)造函數(shù),或是一個構(gòu)造函數(shù)的參數(shù)有好幾個,會造成使用目標對象上的困難與疑惑。若沒有好好進行 refactoring,也可能因此而埋藏許多 bad smell。
另外,倘若是許多構(gòu)造函數(shù),也可能造成要調(diào)用 A 方法時,應(yīng)選用 A 對應(yīng)的構(gòu)造函數(shù),但在使用對象上,可能會用錯構(gòu)造函數(shù)而不自知,若方法中沒有正確的防呆,則可能出現(xiàn)錯誤。(請搭配單元測試的測試案例來輔助)
最后,與原本直接依賴的程序代碼相比較,目標對象的相依對象因此暴露出來,交由外部決定,而喪失了一點封裝的意味。而使用端也不一定知道,要取用此對象時,應(yīng)該要注入哪些相依對象。(請使用 Repository Pattern 或 DI framework 來輔助)
公開屬性(public setter property)
描述:
其實公開屬性與公開構(gòu)造函數(shù)非常類似,通過 public 的 property(property 類型仍為 interface),讓外部在使用目標對象時,可先 setting 目標對象的相依對象,接著才調(diào)用其方法。
而公開屬性通常只會將 setter 公開給外部設(shè)定,getter 則設(shè)定為 private。原因很簡單,外部只需設(shè)定,而不需取用。就像公開構(gòu)造函數(shù),在使用對象之前先傳入初始化對象必備的信息,但目標對象可能將這些信息,存放在 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(); }}到這里,都不影響外部使用目標對象的行為,我們只是在重構(gòu)對象的內(nèi)部方法罷了。事實上,我們可測試性的動作也準備完畢了。(當(dāng)然,建議還是要依賴于接口,實現(xiàn)接口要顧慮的點,比繼承類要輕松的多)
接下來把目光切到測試程序,該如何對 CheckAuthentication 方法進行測試。
首先,將上一篇文章的 StubHash 改為繼承自 Hash,StubAccountDao 改為繼承自 AccountDao,并將原本 public 的方法,加上 override 關(guān)鍵詞,重寫其父類方法內(nèi)容。程序代碼如下:
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(); }}也不難,對吧。接下來,來設(shè)計單元測試,程序代碼如下:
[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 對象,現(xiàn)在則為 MyValidation 對象。里面唯一不同的部分,只有重寫的方法內(nèi)容,其余 MyValidation 就等同于 Validation。(Is-A的關(guān)系)調(diào)試測試一下,就可以確認,程序代碼就跟之前使用 IoC 的方式執(zhí)行沒有太大的差異。
好處:
這個方式最大的好處,是完全不影響外部使用對象的方式。僅透過 protected 與 virtual 來對繼承鏈開放擴充的功能,并且透過這樣的方式,就使得原本直接相依而導(dǎo)致無法測試的問題,獲得解套。
顧慮點:
這是為了測試,且面對 legacy code 所使用的方式,而不是良好的面向?qū)ο笤O(shè)計的方式。IoC 的用意在于面向借口與擴充點的彈性,所以當(dāng)可測試之后,倘若重構(gòu)影響范圍不大,建議讀者朋友還是要將對象改依賴于接口,通過IoC 的方式來設(shè)計對象。
by the way, 同樣為了解決直接相依對象,甚至相依于 static 方法、.net framework 本身的對象(如 DateTime.Now)而導(dǎo)致無法測試的問題,還有另外一個方式,稱為 fake object。這在后面的文章,會再進行較為詳盡的介紹。
結(jié)論
以上幾種用來測試的方式,希望對各位讀者在不同情境下的設(shè)計,可以有所幫助。
而許多延伸的議題,在這系列文章并不會多談,但在實務(wù)應(yīng)用面上,卻是相當(dāng)重要的配套措施。例如一再提到的 DI framework, Repository Pattern,以及通過測試程序來說明對象的使用方式,請讀者在現(xiàn)實設(shè)計系統(tǒng)時,務(wù)必了解這些東西如何讓系統(tǒng)設(shè)計更加完整。
下一篇文章,將介紹怎么樣可以避免每次手工敲打這么啰唆的 stub 對象,怎么針對 static 或 .net framework 本身的對象進行隔離,怎么針對對象與相依接口互動的情況進行測試。
備注:這個系列是我畢業(yè)后時隔一年重新開始進入開發(fā)行業(yè)后對大拿們的博文摘要整理進行學(xué)習(xí)對自我的各個欠缺的方面進行充電記錄博客的過程,非原創(chuàng),特此感謝91 等前輩