基于VS2012 Fakes框架的TDD實(shí)戰(zhàn)——私有成員,靜態(tài)成員模擬
前言
上文書(基于VS2012 Fakes框架的TDD實(shí)戰(zhàn)——接口模擬)把接口模擬的部分演示完了,接口模擬也是Mock框架最基本的功能了吧,比如很易用的Moq框架,就非常容易模擬出接口中定義的操作返回的結(jié)果。
Moq也有局限性,比如不能模擬密封類,不能直接模擬靜態(tài)方法等,而這些需求在微軟VS2012帶來(lái)的Fakes框架中都能得到很好的解決。
需求說(shuō)明
一個(gè)項(xiàng)目的開發(fā)中,最見怪不怪的就是需求的變更了,比如我們這個(gè)用戶名重復(fù)性檢查的功能,它就變了,變化如下:
- 給未激活用戶信息添加有效期屬性,防止用戶名被惡意占用
準(zhǔn)備工作
修改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 /// 激活過(guò)期時(shí)間 11 /// </summary> 12 public DateTime Expiration { get; set; } 13 }
開工
-
編寫測(cè)試用例與實(shí)現(xiàn)代碼
先編寫一個(gè)檢查未激活用戶信息有效性的方法的測(cè)試用例,現(xiàn)在是2012年8月26日,所以定MemberInactive的過(guò)期時(shí)間為2012年8月27日。方便起見,我們先把IsMemberInactiveValid方法的可訪問(wèn)性定為public,用使可以用原來(lái)的方式來(lái)進(jìn)行測(cè)試
1 [TestMethod] 2 public void IsMemberInactiveValid_有效的_過(guò)期時(shí)間大于當(dāng)前時(shí)間() 3 { 4 var memberInactive = new MemberInactive { Expiration = new DateTime(2012, 8, 27) }; 5 Assert.IsTrue(_accountService.IsMemberInactiveValid(memberInactive)); 6 }
在類AccountService編寫IsMemberInactiveValid方法讓測(cè)試通過(guò)
1 public bool IsMemberInactiveValid(MemberInactive memberInactive) 2 { 3 var dtNow = DateTime.Now; 4 return memberInactive.Expiration.CompareTo(dtNow) >= 0; 5 }
-
靜態(tài)屬性的模擬
測(cè)試通過(guò)了,但上面的測(cè)試用例有個(gè)問(wèn)題,今天能跑通過(guò),后天呢,到了28號(hào),就注定是失敗的了,因?yàn)閷?shí)現(xiàn)方法中有一個(gè)外部依賴DateTime.Now,自動(dòng)化測(cè)試中,方法體范圍內(nèi)的所有外部依賴都應(yīng)該被模擬即你要測(cè)的僅是這個(gè)方法內(nèi)的代碼的正確性,不應(yīng)該受外界影響。現(xiàn)在我們來(lái)模擬DateTime.Now,這是一個(gè)靜態(tài)的公共屬性。在mscorlib.dll程序集System命名空間下實(shí)現(xiàn)的。所以需要?jiǎng)?chuàng)建System的Fakes程序集。靜態(tài)成員的模擬將用到Shim類型的模擬類(Fakes框架生成的模擬類有兩種,Stub和Shim,具體請(qǐng)參考官方文檔)

修改上面的測(cè)試用例如下:
1 [TestMethod] 2 public void IsMemberInactiveValid_有效的_過(guò)期時(shí)間大于當(dāng)前時(shí)間() 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的返回值,這時(shí),即使你把系統(tǒng)時(shí)間修改為28號(hào),這個(gè)測(cè)試也能通過(guò),因?yàn)楝F(xiàn)在測(cè)試的運(yùn)行已經(jīng)與系統(tǒng)時(shí)間無(wú)關(guān)了。
-
私有方法的測(cè)試
上面的例子為了承接上篇寫測(cè)試用例的方法把IsMemberInactiveValid方法設(shè)成了public,但實(shí)際上這個(gè)方法應(yīng)該是私有的,現(xiàn)在把方法的可訪問(wèn)性改為private,原來(lái)的測(cè)試用例當(dāng)然是不能通過(guò)的,因?yàn)檫@個(gè)方法找不到了。把測(cè)試用例改為如下:
[TestMethod] public void IsMemberInactiveValid_有效的_過(guò)期時(shí)間大于當(dāng)前時(shí)間() { 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); } }
測(cè)試通過(guò),私有成員的訪問(wèn)用到了PrivateObject,其實(shí)這個(gè)類也沒什么奇特的地方,只是封裝了反射的相關(guān)操作,讓我們調(diào)用更方便些
-
私有方法的模擬
在把調(diào)用IsMemberInactiveValid的代碼加入U(xiǎn)serNameExistsCheck方法之前,千萬(wàn)別忘記了在測(cè)試類初始化的代碼中把IsMemberInactiveValid模擬出來(lái),否則加入之后原來(lái)的測(cè)試用例就有可能無(wú)法通過(guò)了。下面這個(gè)測(cè)試用例就通不過(guò)了
1 [TestMethod] 2 public void UserNameExistsCheck_用戶存在_用戶在用戶數(shù)據(jù)庫(kù)中不存在_and_注冊(cè)需要激活_用戶在未激活用戶數(shù)據(jù)庫(kù)中存在() 3 { 4 var userName = "柳柳英俠"; 5 var configName = "configName"; 6 _member = null; 7 _configInfo.RegisterConfig.NeedActive = true; 8 Assert.IsTrue(_accountService.UserNameExistsCheck(userName, configName)); 9 }
不過(guò)這個(gè)問(wèn)題先放下,我們先來(lái)看看私有成員應(yīng)該怎樣來(lái)模擬,將用到AccountService類的模擬類,因?yàn)檫@個(gè)私有方法是這個(gè)類的成員,如下的測(cè)試用例:
1 [TestMethod] 2 public void UserNameExistsCheck_用戶存在_用戶在用戶數(shù)據(jù)庫(kù)中不存在_and_注冊(cè)需要激活_and_用戶在未激活用戶數(shù)據(jù)庫(kù)中存在_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 }
根據(jù)測(cè)試用例修改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("系統(tǒng)配置信息為空。"); 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 }
測(cè)試通過(guò)。
現(xiàn)在來(lái)解決那個(gè)未通過(guò)的測(cè)試用例,按照TDD的原則,我們不能去修改測(cè)試用例來(lái)使它通過(guò)。
不能通過(guò)的原因也就是IsMemberInactiveValid的模擬沒有在測(cè)試類中進(jìn)行初始化,下面我們來(lái)初始化它。由上面的測(cè)試用例可以看到,在私有成員的模擬中,測(cè)試方法的執(zhí)行結(jié)果必須放在using語(yǔ)句中,而using語(yǔ)句實(shí)質(zhì)也就是自動(dòng)化了IDisposable接口,所以我們完全可以把它拆開,然后手動(dòng)調(diào)用Dispose即可
在測(cè)試類AccountServiceTest添加一個(gè)私有字段來(lái)存儲(chǔ)ShimsContext.Create(),一個(gè)私有字段存儲(chǔ) IsMemberInactiveValid的模擬結(jié)果:1 private IDisposable _shimsContext = ShimsContext.Create(); 2 private bool _isMemberInactiveValid = true;
在標(biāo)記[TestInitialize]的MyTestInitialize方法中添加 IsMemberInactiveValid 方法的模擬
1 ShimAccountService.AllInstances.IsMemberInactiveValidMemberInactive = (@accountService, @memberInactive) => _isMemberInactiveValid;取消標(biāo)記[TestCleanup()]的MyTestCleanup方法的注釋,添加 ShimsContext.Create() 的 Dispose 調(diào)用
1 // 在每個(gè)測(cè)試運(yùn)行完之后,使用 TestCleanup 來(lái)運(yùn)行代碼 2 [TestCleanup()] 3 public void MyTestCleanup() 4 { 5 _shimsContext.Dispose(); 6 }
這樣,初始化完畢,再運(yùn)行全部測(cè)試用例,全綠,心情大好└(^o^)┘

總結(jié)
總結(jié)說(shuō)點(diǎn)什么呢,總結(jié)一下TDD吧
開發(fā)過(guò)程:
- 快速新增一個(gè)測(cè)試
- 運(yùn)行所有的測(cè)試(有時(shí)候只需要運(yùn)行一個(gè)或一部分),發(fā)現(xiàn)新增的測(cè)試不能通過(guò)
- 做一些小小的改動(dòng),盡快地讓測(cè)試程序可運(yùn)行,為此可以在程序中使用一些不合情理的方法
- 運(yùn)行所有的測(cè)試,并且全部通過(guò)
- 重構(gòu)代碼,以消除重復(fù)設(shè)計(jì),優(yōu)化設(shè)計(jì)結(jié)構(gòu)
優(yōu)點(diǎn):
- 在開發(fā)過(guò)程的任意時(shí)刻,都可以生成一個(gè)可以使用,具有一定功能,Bug較少的測(cè)試版本
- 新增的功能不會(huì)破壞已有功能
- 測(cè)試用例已經(jīng)包含業(yè)務(wù)需求和規(guī)則,是最符合實(shí)際,與時(shí)俱進(jìn)的開發(fā)文檔
- 規(guī)則長(zhǎng)期保留并明確
缺點(diǎn):
- 代碼量大大增加(其實(shí)只是把需求先體驗(yàn)在代碼上,傳統(tǒng)的開發(fā)方式先需求體驗(yàn)在腦中)
- 憑空編寫測(cè)試用例(其實(shí)并不憑空,只需要把代碼運(yùn)行的環(huán)境,涉及的底層模塊想清楚,就能立即體現(xiàn)到測(cè)試用例上)
其實(shí)說(shuō)白了,TDD與先代碼后測(cè)試的開發(fā)方式的區(qū)別,只是前者是把寫代碼前在腦中的想法體現(xiàn)在測(cè)試用例上,一個(gè)動(dòng)手寫了,一個(gè)在腦中構(gòu)思而已,就這么簡(jiǎn)單,只要立即動(dòng)手把想法以測(cè)試用例展現(xiàn)出來(lái),就跨出TDD的第一步了,勇敢的跨出第一步吧
源碼下載
作者:郭明鋒
Q群:MVC EF技術(shù)交流(5008599)
OSharp開發(fā)框架交流(85895249)
出處:http://www.rzrgm.cn/guomingfeng
聲明:本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁(yè)面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。


浙公網(wǎng)安備 33010602011771號(hào)