在上節中,完成了第一個單元測試,研究了各種特性,在本節,將介紹一些更實際的例子。SUT依賴於一個不可操控的對象,最常見的例子是文件系統,線程,內存和時間等。
本系列將分成3節:
- 單元測試基礎知識
- 打破依賴,使用模擬對象,樁對象,隔離框架
- 創建優秀的單元測試
本節索引:
偽對象(fake) 樁對象(stub) 模擬對象(mock)
偽對象是一個通用術語,它即可指樁對象,也可指模擬對象。
樁對象是指對系統中現有依賴項的一個替代品,可人為控制。
模擬對象是用來決定一個單元測試是通過還是失敗的偽對象。
說明:fake是stub和mock的統稱,因為看起來都像是真的對象。如果是用來檢查交互的就是模擬對象,否則就是樁對象
樁對象:
模擬對象:
- 外部依賴(系統中代碼與其交互的對象,而且無法對其做人為控制)
- 反測試(而一旦測試中存在外部依賴,那么這個測試就是一個集成測試。運行慢,需要配置,依賴異常)
如何處理?
本質上都是外部依賴導致的,所以要做的是消除依賴。
- 分析接口
- 實現可人為控制的接口
注入樁對象
- 在構造函數上接受一個接口,並保存在一個字段里,以備后用。
- 保存在屬性上
- 在調用方法前,使用方法參數,工廠類,依賴注入等
隱藏樁對象(由於生產環境等其他原因,我們不希望暴露樁對象)
- 使用條件編譯
- 使用條件特性
- 使用internal和[InternalVisibleTo]
使用樁對象(適用於模擬返回值,不適用於檢查對象間的交互情況。)
這是非常常見的方式,但是這種方式受限制很多,如文件需要配置,運行慢。
public class Config { public bool IsCheck(string name) { var str = File.ReadAllText("1.txt"); return str == name;//此處可能是大量的邏輯處理 } }
改寫注入
public class Config { private IManager manager; //提供注入接口 public Config(IManager manager) { this.manager = manager; } public bool IsCheck(string name) { var str = manager.GetConfig(); return str == name; } } //真實的實現 public class FileManager : IManager { public string GetConfig() { return File.ReadAllText("1.txt"); } } //測試使用的實現 public class StubManager : IManager { public string GetConfig() { return "str"; } } //抽象出的接口 public interface IManager { string GetConfig(); }
測試代碼
[TestClass] public class ConfigTests { private Config config; [TestInitialize] public void Init() { config = new Config(new StubManager()); } [TestMethod] public void IsCheckTest() { Assert.IsTrue(config.IsCheck("str")); } [TestCleanup] public void Clean() { config = null; } }
使用模擬對象(適用於對象之間的交互)
當上面的方法返回false的時候,需要調用別的web服務記錄下。而web服務還未開發好,即使開發好了,測試的時間也會變長很多。
這里其實也體現了,stub的優點,可以任意的控制返回結果。
新建一個mock
public class Config { private IManager manager; public IWeb Web { get; set; } public Config(IManager manager) { this.manager = manager; } public bool IsCheck(string name) { var str = manager.GetConfig(); var rst = str == name; if (!rst) Web.Log("錯誤"); return rst; } } /// <summary> /// 模擬對象 /// </summary> public class MockWeb : IWeb { public string Erro { get; set; } public void Log(string erro) { Erro = erro; } } public interface IWeb { void Log(string erro); }
測試代碼
[TestClass] public class WebTests { [TestMethod] public void LogTest() { var web = new MockWeb(); //注入的方式非常多 var config = new Config(new StubManager()) { Web = web }; config.IsCheck("s"); //最終斷言的是模擬對象。 Assert.AreEqual("錯誤", web.Erro); } }
注意:一個測試只有一個mock,其他偽對象都是stub,如果存在多個mock,說明這個單元測試是在測多個事情,這樣會讓測試變得復雜和脆弱。
隔離框架簡介
手寫stub和mock非常麻煩耗時,而且不易看懂等缺點。
隔離框架是可以方便的新建stub和mock的一組可編程API。
.net下常見的有Rhino Mocks,Moq
這里使用RhinoMocks做示例(將使用錄制回放模式和操作斷言2種)
錄制回放
新建mock對象
來實現一個和上面mock的例子
[TestMethod] public void LogMockTest() { var mocks = new MockRepository();
//嚴格模擬對象 var mockWeb = mocks.StrictMock<IWeb>(); using (mocks.Record())//錄制預期行為 { mockWeb.Log("錯誤"); } var config = new Config(new StubManager()) { Web = mockWeb }; config.IsCheck("s"); mocks.Verify(mockWeb); }
嚴格模擬對象:是指只要出現預期行為以外的情況,就報錯。
非嚴格模擬對象:是指執行到最后一行,才會報錯。
新建stub對象
[TestMethod] public void LogStubTest() { var mocks = new MockRepository(); //非嚴格對象 var mockWeb = mocks.DynamicMock<IWeb>(); //樁對象 var stubManager = mocks.Stub<IManager>(); using (mocks.Record()) { mockWeb.Log("錯誤1"); stubManager.GetConfig(); LastCall.Return("str1"); //錄制樁對象返回值 } var config = new Config(stubManager) { Web = mockWeb }; config.IsCheck("str"); mocks.Verify(stubManager); //樁對象不會導致測試失敗 mocks.VerifyAll(); //啟用非嚴格對象,測試直到這里才會確認是否報錯 }
操作斷言
[TestMethod] public void LogReplayTest() { var mocks = new MockRepository(); var mockWeb = mocks.DynamicMock<IWeb>(); var config = new Config(new StubManager()) { Web = mockWeb }; //開始操作模式 mocks.ReplayAll(); config.IsCheck("str1"); //使用Rhino Mocks斷言 mockWeb.AssertWasCalled(o => o.Log("錯誤")); }
注意:使用框架創建的動態偽對象,肯定沒手工編寫的偽對象執行效率高。
本文作者:Never、C
本文鏈接:http://www.cnblogs.com/neverc/p/4749197.html