利用MOQ來偽裝和隔離被依賴對象,從而提高被測對象的測試效果。
MOQ的安裝
通過http://code.google.com/p/moq可以下載MOQ的最新版本。在SSL項目中,我們使用的是MOQ 3.1.416.3版本。在SCM中項目目錄下的Lib目錄下有該工具的二進制版本。直接在單元測試項目中引用即可。
準備工作
如果你需要測試項目中的Internal成員,你需要在AssemblyInfo.cs中添加如下的Attribute:
#if DEBUG
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2,PublicKey=00240000048000009400000006020000002400005253" +
"41310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266" +
"654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c" +
"4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
#endif
開始測試
一個單元測試的一般流程:
一般情況下,一個單元測試應該被分割為如下四個步驟:
準備
搭建環(huán)境
構造被測對象
初始化被測對象
構造Mock對象
初始化Mock對象
連接被測對象和依賴項
聲明期待
配置Mock(Mock<T>.Setup)對象以聲明該Mock對象期待被怎樣調用。
執(zhí)行測試
調用被測對象的方法,完成測試步驟
校驗測試結果
調用校驗方法(Mock<T>.VerifyAll)對Mock對象上的期待動作進行校驗。
使用Assert方法對被測對象的狀態(tài)進行校驗。
一個單元測試的例子:
view sourceprint?[TestMethod]
public void TestNavigationSyncWithSelection()
{
// 測試如果修改Selection,那么NavigationService.MoveCurrentTo方法應該被調用。
// 1. 準備
// 1.1 搭建環(huán)境
var c = new ServiceContainer();
var dataManager = new SpreadSheetDataManager(32, 8);
// 1.2 構造被測對象
var selectionService = new SelectionService();
// 1.3 初始化被測對象
// 1.4 構造Mock對象
var mockNavigationService = new Mock<INavigationService>();
// 1.5 初始化和配置Mock對象
c.AddService(mockNavigationService);
mockNavigationService.Setup(s => s.CanMoveCurrentTo(It.IsAny<CellPosition>()))
.Returns(true);
// 1.6 連接被測對象和依賴項
c.AddService<ISelectionService>(selectionService); // 這里隱式的將SelectionService和NavigationService連接在一起了。因為他們都被放到了一個容器里面。
(selectionService as IService).Attach(c, dataManager);
// 2. 聲明期待
mockNavigationService.Setup(s => s.MoveCurrentTo(new CellPosition(0, 1))); // navigationService的MoveCurrentTo方法期待被調用,并且參數(shù)為【0,1】。
// 3. 執(zhí)行測試
selectionService.Select(new CellRange(0, 1, 2, 2));
// 4. 校驗
// 4.1 校驗Mock對象期待的動作被正確的調用了。
mockNavigationService.VerifyAll();
// 4.2 校驗被測對象的狀態(tài)。
Assert.AreEqual(new CellRange(0, 1, 2, 2), selectionService.CurrentSelection);
}
推薦的單元測試寫法
目前的單元測試中,往往準備工作很復雜,反而真正測試的工作比較簡單。就像上面的例子中,準備的代碼寫了8行,其它真正測試所關心的代碼卻只有4行。這是一個非常不舒服的狀態(tài)。但是,我也沒有找到更好的方式來解決這個問題。只能說在架構上讓各個模塊的依賴盡可能的小,從而減少準備工作的量。
另一方面,通過在代碼中適當?shù)脑黾訋仔凶⑨,可以很好的幫助閱讀的人找到重點。我覺的如果整個團隊都采用一致的編碼習慣,閱讀效率會提高很多。如下是上面的例子去除了多余的注釋后的版本。
[TestMethod]
public void TestNavigationSyncWithSelection()
{
// 測試如果修改Selection,那么NavigationService.MoveCurrentTo方法應該被調用。 << 簡要的注釋描述測試的重點,很多時候“人話”還是簡練很多
// Prepare
var c = new ServiceContainer();
var dataManager = new SpreadSheetDataManager(32, 8);
var selectionService = new SelectionService();
var mockNavigationService = new Mock<INavigationService>();
c.AddService(mockNavigationService);
mockNavigationService.Setup(s => s.CanMoveCurrentTo(It.IsAny<CellPosition>()))
.Returns(true);
c.AddService<ISelectionService>(selectionService);
(selectionService as IService).Attach(c, dataManager);
// Expect
mockNavigationService.Setup(s => s.MoveCurrentTo(new CellPosition(0, 1)));
// Act
selectionService.Select(new CellRange(0, 1, 2, 2));
// Verity
mockNavigationService.VerifyAll();
Assert.AreEqual(new CellRange(0, 1, 2, 2), selectionService.CurrentSelection);
}