文章目錄
Unit Test 該拿 static 屬性及欄位怎麼辦? - 使用 PrivateType
自從上完第二次 TDD 課程後,對於新專案的開發充滿著信心,躍躍欲試不算新學到但有新理解的技能,只是過程還是跌跌撞撞、踩雷不斷,也許不該說是踩雷,說是對測試的相關工具還有觀念都沒有很熟悉的關係比較正確。
這也讓我想起有次參加 曹祖聖 老師在一場研討會中,提到有次他在研究 System Center Operations Manager 時,第一次環境架設就一切順利,完全沒遇到問題,但他卻一點開心的感覺也沒有,反而是很失落,因為他知道他沒有真的學到東西,透過錯誤解決以及發想解決方案的過程才會讓他學到更多,同樣的想法也影響著我,透過寫測試時遇到的各式問題不僅讓我更了解測試也讓我更踏實
回到今天的主題,測試時遇到 static field 或是 property 該如何處理?
基本環境
- 一個 restful Web Api 只有 Post 有動作,其中引用 nlog 來紀錄收到 request 的時間與收到的參數
程式碼
public class ValuesController : ApiController { private static ILogger logger = LogManager.GetLogger("ValuesController"); public IHttpActionResult Post([FromBody] string value) { logger.Debug($"EventTime:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} Value={value}"); if (string.IsNullOrEmpty(value)) return BadRequest(); else return Ok(); } }
如何測試?
經過三天 TDD 的訓練後,第一眼看到程式碼,我腦中浮現的測試程式大概長得像下面程式
目標程式加入 constructor
public ValuesController() { }
為 static 資源加入 seam
將 static 資源初始化動作移至 constructor,讓後續有空隙可以將假造物件塞入
private static ILogger logger; public ValuesController() { logger = LogManager.GetLogger("ValuesController"); }
加入允許傳入 ILogger 參數的 constructor 並讓無參數 constructor 呼叫
這邊無參數 constructor 呼叫傳入 ILogger 參數的 constructor 可以讓原本呼叫該 api 的程式碼不用異動
public ValuesController() : this(LogManager.GetLogger("ValuesController")) { } public ValuesController(ILogger _logger) { logger = _logger; }
從測試程式傳入 ILogger 物件
搭配
NSubstitute
產生虛擬物件,關於如何驗證 IHttpActionResult 細節可以參考 Unit Test 如何驗證 ASP.NET Web Api 的 IHttpActionResult[TestMethod] public void Post_StringEmpty_Return_BadRequest() { //arrange var logger = Substitute.For<ILogger>(); var target = new ValuesController(logger); var expected = typeof(BadRequestResult); //act var actualAction = target.Post(string.Empty); var actual = actualAction as BadRequestResult; //assert Assert.IsNotNull(actual); Assert.IsInstanceOfType(actual, expected); }
有改善空間嗎?
測試程式本身應該滿簡潔,測試目標程式的修改幅度也不大,說實話我覺得很棒了,但就是掩不住好奇的心想知道其他人是怎麼做的,下面提供另一個做法(我也不知道是好是壞,請大家自行斟酌衡量)
- 使用 PrivateType 來設定 static field 或是 property
不用修改 測試目標程式
public class ValuesController : ApiController { private static ILogger logger = LogManager.GetLogger("ValuesController"); public IHttpActionResult Post([FromBody] string value) { logger.Debug($"EventTime:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")};Value={value}"); if (string.IsNullOrEmpty(value)) return BadRequest(); else return Ok(); } }
使用
NSubstitute
產生 ILogger 虛擬物件var logger = Substitute.For<ILogger>();
建立測試目標程式的 PrivateType 物件
PrivateType valueController = new PrivateType(typeof(ValuesController));
設定 測試目標程式 static field
valueController.SetStaticFieldOrProperty("logger", logger);
最終程式碼
[TestMethod] public void Post_StringEmpty_Return_BadRequest() { //arrange var logger = Substitute.For<ILogger>(); var target = new ValuesController(); PrivateType valueController = new PrivateType(typeof(ValuesController)); valueController.SetStaticFieldOrProperty("logger", logger); var expected = typeof(BadRequestResult); //act var actualAction = target.Post(string.Empty); var actual = actualAction as BadRequestResult; //assert Assert.IsNotNull(actual); Assert.IsInstanceOfType(actual, expected); }
心得
我測試下來 PrivateType 針對 static 資源有 set、 get 跟 invoke (針對 method) 的 api 可以使用,但有個重點是 static
,不過不覺得它的名稱取得不好,叫 PrivateType 但事實上 non-private 也可以用(誤),使用 PrivateType 好處是完全不需要異動到測試目標程式碼,這讓測試程式寫起來更乾淨,提供給大家參考看看,詳細介紹請參考 PrivateType Class(說實話,介紹也沒有很詳細啦)
參考資訊
文章作者 Yowko Tsai
上次更新 2021-10-14
授權合約
本部落格 (Yowko's Notes) 所有的文章內容(包含圖片),任何轉載行為,必須通知並獲本部落格作者 (Yowko Tsai) 的同意始得轉載,且轉載皆須註明出處與作者。
Yowko's Notes 由 Yowko Tsai 製作,以創用CC 姓名標示-非商業性-相同方式分享 3.0 台灣 授權條款 釋出。