前言
上文書(基於VS2012 Fakes框架的TDD實戰——接口模擬)把接口模擬的部分演示完了,接口模擬也是Mock框架最基本的功能了吧,比如很易用的Moq框架,就非常容易模擬出接口中定義的操作返回的結果。
Moq也有局限性,比如不能模擬密封類,不能直接模擬靜態方法等,而這些需求在微軟VS2012帶來的Fakes框架中都能得到很好的解決。
需求說明
一個項目的開發中,最見怪不怪的就是需求的變更了,比如我們這個用戶名重復性檢查的功能,它就變了,變化如下:
- 給未激活用戶信息添加有效期屬性,防止用戶名被惡意占用
准備工作
修改MemberInactive類如下:
1 public class MemberInactive : Entity 2 { 3 public string UserName { get; set; } 4 5 public string Password { get; set; } 6 7 public string Email { get; set; } 8 9 /// <summary> 10 /// 激活過期時間 11 /// </summary> 12 public DateTime Expiration { get; set; } 13 }
開工
-
編寫測試用例與實現代碼
先編寫一個檢查未激活用戶信息有效性的方法的測試用例,現在是2012年8月26日,所以定MemberInactive的過期時間為2012年8月27日。方便起見,我們先把IsMemberInactiveValid方法的可訪問性定為public,用使可以用原來的方式來進行測試
1 [TestMethod] 2 public void IsMemberInactiveValid_有效的_過期時間大於當前時間() 3 { 4 var memberInactive = new MemberInactive { Expiration = new DateTime(2012, 8, 27) }; 5 Assert.IsTrue(_accountService.IsMemberInactiveValid(memberInactive)); 6 }
在類AccountService編寫IsMemberInactiveValid方法讓測試通過
1 public bool IsMemberInactiveValid(MemberInactive memberInactive) 2 { 3 var dtNow = DateTime.Now; 4 return memberInactive.Expiration.CompareTo(dtNow) >= 0; 5 }
-
靜態屬性的模擬
測試通過了,但上面的測試用例有個問題,今天能跑通過,后天呢,到了28號,就注定是失敗的了,因為實現方法中有一個外部依賴DateTime.Now,自動化測試中,方法體范圍內的所有外部依賴都應該被模擬即你要測的僅是這個方法內的代碼的正確性,不應該受外界影響。現在我們來模擬DateTime.Now,這是一個靜態的公共屬性。在mscorlib.dll程序集System命名空間下實現的。所以需要創建System的Fakes程序集。靜態成員的模擬將用到Shim類型的模擬類(Fakes框架生成的模擬類有兩種,Stub和Shim,具體請參考官方文檔)
修改上面的測試用例如下:
1 [TestMethod] 2 public void IsMemberInactiveValid_有效的_過期時間大於當前時間() 3 { 4 var memberInactive = new MemberInactive { Expiration = new DateTime(2012, 8, 27) }; 5 //Assert.IsTrue(_accountService.IsMemberInactiveValid(memberInactive)); 6 using (ShimsContext.Create()) 7 { 8 ShimDateTime.NowGet = () => new DateTime(2012, 8, 26); 9 Assert.IsTrue(_accountService.IsMemberInactiveValid(memberInactive)); 10 } 11 }
第8行即模擬了DateTime.Now的返回值,這時,即使你把系統時間修改為28號,這個測試也能通過,因為現在測試的運行已經與系統時間無關了。
-
私有方法的測試
上面的例子為了承接上篇寫測試用例的方法把IsMemberInactiveValid方法設成了public,但實際上這個方法應該是私有的,現在把方法的可訪問性改為private,原來的測試用例當然是不能通過的,因為這個方法找不到了。把測試用例改為如下:
[TestMethod] public void IsMemberInactiveValid_有效的_過期時間大於當前時間() { var memberInactive = new MemberInactive { Expiration = new DateTime(2012, 8, 27) }; using (ShimsContext.Create()) { ShimDateTime.NowGet = () => new DateTime(2012, 8, 26); var po = new PrivateObject(new AccountService()); var result = po.Invoke("IsMemberInactiveValid", new object[] {memberInactive}); Assert.IsTrue((bool) result); } }
測試通過,私有成員的訪問用到了PrivateObject,其實這個類也沒什么奇特的地方,只是封裝了反射的相關操作,讓我們調用更方便些
-
私有方法的模擬
在把調用IsMemberInactiveValid的代碼加入UserNameExistsCheck方法之前,千萬別忘記了在測試類初始化的代碼中把IsMemberInactiveValid模擬出來,否則加入之后原來的測試用例就有可能無法通過了。下面這個測試用例就通不過了
1 [TestMethod] 2 public void UserNameExistsCheck_用戶存在_用戶在用戶數據庫中不存在_and_注冊需要激活_用戶在未激活用戶數據庫中存在() 3 { 4 var userName = "柳柳英俠"; 5 var configName = "configName"; 6 _member = null; 7 _configInfo.RegisterConfig.NeedActive = true; 8 Assert.IsTrue(_accountService.UserNameExistsCheck(userName, configName)); 9 }
不過這個問題先放下,我們先來看看私有成員應該怎樣來模擬,將用到AccountService類的模擬類,因為這個私有方法是這個類的成員,如下的測試用例:
1 [TestMethod] 2 public void UserNameExistsCheck_用戶存在_用戶在用戶數據庫中不存在_and_注冊需要激活_and_用戶在未激活用戶數據庫中存在_and_未激活用戶信息有效() 3 { 4 var userName = "柳柳英俠"; 5 var configName = "configName"; 6 _member = null; 7 _configInfo.RegisterConfig.NeedActive = true; 8 using (ShimsContext.Create()) 9 { 10 ShimAccountService.AllInstances.IsMemberInactiveValidMemberInactive = (@accountService, @memberInactive) => true; 11 Assert.IsTrue(_accountService.UserNameExistsCheck(userName, configName)); 12 } 13 }
根據測試用例修改UserNameExistsCheck方法代碼如下(第26行)
1 public bool UserNameExistsCheck(string userName, string configName) 2 { 3 if (string.IsNullOrEmpty(userName)) 4 { 5 throw new ArgumentNullException("userName"); 6 } 7 if (string.IsNullOrEmpty(configName)) 8 { 9 throw new ArgumentNullException("configName"); 10 } 11 var member = MemberDao.GetByName(userName); 12 if (member != null) 13 { 14 return true; 15 } 16 var configInfo = ConfigInfoDao.GetByName(configName); 17 if (configInfo == null) 18 { 19 throw new NullReferenceException("系統配置信息為空。"); 20 } 21 if (!configInfo.RegisterConfig.NeedActive) 22 { 23 return false; 24 } 25 var memberInactive = MemberInactiveDao.GetByName(userName); 26 if (memberInactive != null && IsMemberInactiveValid(memberInactive)) 27 { 28 return true; 29 } 30 return false; 31 }
測試通過。
現在來解決那個未通過的測試用例,按照TDD的原則,我們不能去修改測試用例來使它通過。
不能通過的原因也就是IsMemberInactiveValid的模擬沒有在測試類中進行初始化,下面我們來初始化它。由上面的測試用例可以看到,在私有成員的模擬中,測試方法的執行結果必須放在using語句中,而using語句實質也就是自動化了IDisposable接口,所以我們完全可以把它拆開,然后手動調用Dispose即可
在測試類AccountServiceTest添加一個私有字段來存儲ShimsContext.Create(),一個私有字段存儲 IsMemberInactiveValid的模擬結果:1 private IDisposable _shimsContext = ShimsContext.Create(); 2 private bool _isMemberInactiveValid = true;
在標記[TestInitialize]的MyTestInitialize方法中添加 IsMemberInactiveValid 方法的模擬
1 ShimAccountService.AllInstances.IsMemberInactiveValidMemberInactive = (@accountService, @memberInactive) => _isMemberInactiveValid;
取消標記[TestCleanup()]的MyTestCleanup方法的注釋,添加 ShimsContext.Create() 的 Dispose 調用
1 // 在每個測試運行完之后,使用 TestCleanup 來運行代碼 2 [TestCleanup()] 3 public void MyTestCleanup() 4 { 5 _shimsContext.Dispose(); 6 }
這樣,初始化完畢,再運行全部測試用例,全綠,心情大好└(^o^)┘
總結
總結說點什么呢,總結一下TDD吧
開發過程:
- 快速新增一個測試
- 運行所有的測試(有時候只需要運行一個或一部分),發現新增的測試不能通過
- 做一些小小的改動,盡快地讓測試程序可運行,為此可以在程序中使用一些不合情理的方法
- 運行所有的測試,並且全部通過
- 重構代碼,以消除重復設計,優化設計結構
優點:
- 在開發過程的任意時刻,都可以生成一個可以使用,具有一定功能,Bug較少的測試版本
- 新增的功能不會破壞已有功能
- 測試用例已經包含業務需求和規則,是最符合實際,與時俱進的開發文檔
- 規則長期保留並明確
缺點:
- 代碼量大大增加(其實只是把需求先體驗在代碼上,傳統的開發方式先需求體驗在腦中)
- 憑空編寫測試用例(其實並不憑空,只需要把代碼運行的環境,涉及的底層模塊想清楚,就能立即體現到測試用例上)
其實說白了,TDD與先代碼后測試的開發方式的區別,只是前者是把寫代碼前在腦中的想法體現在測試用例上,一個動手寫了,一個在腦中構思而已,就這么簡單,只要立即動手把想法以測試用例展現出來,就跨出TDD的第一步了,勇敢的跨出第一步吧
源碼下載