在上節中,完成了第一個單元測試,研究了各種特性,在本節,將介紹一些更實際的例子。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
