基于VS2012 Fakes框架的TDD實(shí)戰(zhàn)——接口模擬
前言
最近團(tuán)隊(duì)要嘗試TDD(測(cè)試驅(qū)動(dòng)開發(fā))的實(shí)踐,很多人習(xí)慣了先代碼后測(cè)試的流程,對(duì)于TDD總心存恐懼,認(rèn)為沒有代碼的情況下寫測(cè)試代碼時(shí)被架空了,沒法寫下來,其實(shí),根據(jù)個(gè)人實(shí)踐經(jīng)驗(yàn),TDD并不可怕,還很可愛,只要你真正去實(shí)踐了幾十個(gè)測(cè)試用例之后,你會(huì)愛上這種開發(fā)方式的。微軟對(duì)于TDD的開發(fā)方式是大力支持和推薦的,新發(fā)布的VS2012的團(tuán)隊(duì)模板就是根據(jù)。新的Visual Studio 2012給我們帶來了Fakes框架,這是一個(gè)針對(duì)代碼測(cè)試時(shí)對(duì)測(cè)試的外界依賴(如數(shù)據(jù)庫,文件等)進(jìn)行模擬的Mock框架,用上了之后,我立即從Moq的陣營中叛變了^_^。截止到寫此文的時(shí)間,網(wǎng)上還沒有一篇關(guān)于Fakes框架的文章(除了“VS11將擁有更好的單元測(cè)試工具和Fakes框架”這篇介紹性的之外),就讓我們來慢慢摸索著用吧。廢話少說,下面我們就來一步一步的使用Visual Studio 2012的Fakes框架來實(shí)戰(zhàn)一把TDD。
需求說明
我們要做的是一個(gè)普通的用戶注冊(cè)中“檢查用戶名是否存在”的功能,需求如下:
- 用戶名不能重復(fù)
- 可設(shè)置是否啟用郵件激活,如果不啟用郵件激活,則直接在“正式用戶信息表”中檢查,反之則還要進(jìn)入“未激活用戶信息表”中進(jìn)行查詢
項(xiàng)目結(jié)構(gòu)

先分解一下項(xiàng)目的結(jié)構(gòu),還是傳統(tǒng)的三層結(jié)構(gòu),從底層到上層:
- Liuliu.Components.Tools:通用工具組件
- Liuliu.Components.Data:通用數(shù)據(jù)訪問組件,目前只定義了一個(gè)數(shù)據(jù)訪問接口的通用基接口IRepository
- Liuliu.Demo.Core.Models:數(shù)據(jù)實(shí)體類,分兩個(gè)模塊,賬戶模塊(Account)與通用模塊(Common)
- Liuliu.Demo.Core:業(yè)務(wù)核心層,里面包含Business與DataAccess兩個(gè)子層,DataAccess實(shí)現(xiàn)實(shí)體類的數(shù)據(jù)訪問,Business層實(shí)現(xiàn)模塊的業(yè)務(wù)邏輯,因?yàn)闇y(cè)試的過程中數(shù)據(jù)訪問層的數(shù)據(jù)庫實(shí)現(xiàn)會(huì)用Fakes框架來模擬,所以數(shù)據(jù)訪問層只提供了接口,不提供實(shí)現(xiàn),Business只調(diào)用了DataAccess的接口。我們要做的工作就是用Fakes框架來模擬數(shù)據(jù)訪問層,用TDD的方式來編寫B(tài)usiness中的業(yè)務(wù)實(shí)現(xiàn)
- Liuliu.Demo.Core.Business.UnitTest:?jiǎn)卧獪y(cè)試項(xiàng)目,存放著測(cè)試Business實(shí)現(xiàn)的測(cè)試用例。
- Liuliu.Demo.Consoles:用戶操作控制臺(tái),功能實(shí)現(xiàn)后進(jìn)行用戶操作的UI項(xiàng)目
其他的項(xiàng)目與測(cè)試無關(guān),略過。
開發(fā)準(zhǔn)備
應(yīng)用代碼準(zhǔn)備
Entity:實(shí)體類的通用數(shù)據(jù)結(jié)構(gòu)
1 /// <summary> 2 /// 數(shù)據(jù)實(shí)體類基類,定義數(shù)據(jù)庫存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)的通用部分 3 /// </summary> 4 public abstract class Entity 5 { 6 /// <summary> 7 /// 編號(hào) 8 /// </summary> 9 public int Id { get; set; } 10 11 /// <summary> 12 /// 是否邏輯刪除(相當(dāng)于回收站,非物理刪除) 13 /// </summary> 14 public bool IsDelete { get; set; } 15 16 /// <summary> 17 /// 添加時(shí)間 18 /// </summary> 19 public DateTime AddDate { get; set; } 20 }
IRepository:通用數(shù)據(jù)訪問接口,簡(jiǎn)單起見,只寫了幾個(gè)增刪改查的接口
1 /// <summary> 2 /// 定義倉儲(chǔ)模式中的數(shù)據(jù)標(biāo)準(zhǔn)操作,其實(shí)現(xiàn)類是倉儲(chǔ)類型。 3 /// </summary> 4 /// <typeparam name="TEntity">要實(shí)現(xiàn)倉儲(chǔ)的類型</typeparam> 5 public interface IRepository<TEntity> where TEntity : Entity 6 { 7 #region 公用方法 8 9 /// <summary> 10 /// 插入實(shí)體記錄 11 /// </summary> 12 /// <param name="entity"> 實(shí)體對(duì)象 </param> 13 /// <param name="isSave"> 是否執(zhí)行保存 </param> 14 /// <returns> 操作影響的行數(shù) </returns> 15 int Insert(TEntity entity, bool isSave = true); 16 17 /// <summary> 18 /// 刪除實(shí)體記錄 19 /// </summary> 20 /// <param name="entity"> 實(shí)體對(duì)象 </param> 21 /// <param name="isSave"> 是否執(zhí)行保存 </param> 22 /// <returns> 操作影響的行數(shù) </returns> 23 int Delete(TEntity entity, bool isSave = true); 24 25 /// <summary> 26 /// 更新實(shí)體記錄 27 /// </summary> 28 /// <param name="entity"> 實(shí)體對(duì)象 </param> 29 /// <param name="isSave"> 是否執(zhí)行保存 </param> 30 /// <returns> 操作影響的行數(shù) </returns> 31 int Update(TEntity entity, bool isSave = true); 32 33 /// <summary> 34 /// 提交當(dāng)前的Unit Of Work事務(wù),作用與 IUnitOfWork.Commit() 相同。 35 /// </summary> 36 /// <returns>提交事務(wù)影響的行數(shù)</returns> 37 int Commit(); 38 39 /// <summary> 40 /// 查找指定編號(hào)的實(shí)體記錄 41 /// </summary> 42 /// <param name="id"> 指定編號(hào) </param> 43 /// <returns> 符合編號(hào)的記錄,不存在返回null </returns> 44 TEntity GetById(object id); 45 46 /// <summary> 47 /// 查找指定名稱的實(shí)體記錄,注意:如實(shí)體無名稱屬性則不支持 48 /// </summary> 49 /// <param name="name">名稱</param> 50 /// <returns>符合名稱的記錄,不存在則返回null</returns> 51 /// <exception cref="NotSupportedException">當(dāng)對(duì)應(yīng)實(shí)體無名稱時(shí)引發(fā)將引發(fā)異常</exception> 52 TEntity GetByName(string name); 53 54 #endregion 55 }
Member:實(shí)體類——用戶信息
1 /// <summary> 2 /// 實(shí)體類——用戶信息 3 /// </summary> 4 public class Member : Entity 5 { 6 public string UserName { get; set; } 7 8 public string Password { get; set; } 9 10 public string Email { get; set; } 11 }
MemberInactive:實(shí)體類——未激活用戶信息
1 /// <summary> 2 /// 實(shí)體類——未激活用戶信息 3 /// </summary> 4 public class MemberInactive : Entity 5 { 6 public string UserName { get; set; } 7 8 public string Password { get; set; } 9 10 public string Email { get; set; } 11 }
ConfigInfo:實(shí)體類——系統(tǒng)配置信息
1 /// <summary> 2 /// 實(shí)體類——系統(tǒng)配置信息 3 /// </summary> 4 public class ConfigInfo : Entity 5 { 6 public ConfigInfo() 7 { 8 RegisterConfig = new RegisterConfig(); 9 } 10 11 public RegisterConfig RegisterConfig { get; set; } 12 } 13 14 15 public class RegisterConfig 16 { 17 /// <summary> 18 /// 注冊(cè)時(shí)是否需要Email激活 19 /// </summary> 20 public bool NeedActive { get; set; } 21 22 /// <summary> 23 /// 激活郵件有效期,單位:分鐘 24 /// </summary> 25 public int ActiveTimeout { get; set; } 26 27 /// <summary> 28 /// 允許同一Email注冊(cè)不同會(huì)員 29 /// </summary> 30 public bool EmailRepeat { get; set; } 31 }
IMemberDao:數(shù)據(jù)訪問接口——用戶信息,僅添加IRepository不滿足的接口
1 /// <summary> 2 /// 數(shù)據(jù)訪問接口——用戶信息 3 /// </summary> 4 public interface IMemberDao : IRepository<Member> 5 { 6 /// <summary> 7 /// 由電子郵箱查找用戶信息 8 /// </summary> 9 /// <param name="email"> 電子郵箱地址 </param> 10 /// <returns> </returns> 11 IEnumerable<Member> GetByEmail(string email); 12 }
IMemberInactiveDao:數(shù)據(jù)訪問接口——未激活用戶信息,僅添加IRepository不滿足的接口
1 /// <summary> 2 /// 數(shù)據(jù)訪問接口——未激活用戶信息 3 /// </summary> 4 public interface IMemberInactiveDao : IRepository<MemberInactive> 5 { 6 /// <summary> 7 /// 由電子郵箱獲取未激活的用戶信息 8 /// </summary> 9 /// <param name="email"> 電子郵箱地址 </param> 10 /// <returns> </returns> 11 IEnumerable<MemberInactive> GetByEmail(string email); 12 }
IConfigInfoDao:數(shù)據(jù)訪問接口——系統(tǒng)配置,無額外需求的接口,所以為空接口
1 /// <summary> 2 /// 數(shù)據(jù)訪問接口——系統(tǒng)配置信息 3 /// </summary> 4 public interface IConfigInfoDao : IRepository<ConfigInfo> 5 { }
IAccountContract:賬戶模塊業(yè)務(wù)契約——定義了三個(gè)操作,用作注冊(cè)前的數(shù)據(jù)檢查和注冊(cè)提交
1 /// <summary> 2 /// 核心業(yè)務(wù)契約——賬戶模塊 3 /// </summary> 4 public interface IAccountContract 5 { 6 /// <summary> 7 /// 用戶名重復(fù)檢查 8 /// </summary> 9 /// <param name="userName">用戶名</param> 10 /// <param name="configName">系統(tǒng)配置名稱</param> 11 /// <returns></returns> 12 bool UserNameExistsCheck(string userName, string configName); 13 14 /// <summary> 15 /// 電子郵箱重復(fù)檢查 16 /// </summary> 17 /// <param name="email">電子郵箱</param> 18 /// <param name="configName">系統(tǒng)配置名稱</param> 19 /// <returns></returns> 20 bool EmailExistsCheck(string email, string configName); 21 22 /// <summary> 23 /// 用戶注冊(cè) 24 /// </summary> 25 /// <param name="model">注冊(cè)信息模型</param> 26 /// <param name="configName">系統(tǒng)配置名稱</param> 27 /// <returns></returns> 28 RegisterResults Register(Member model, string configName); 29 }
以上代碼本來想收起來的,但測(cè)試時(shí)代碼展開老失效,所以辛苦大家劃了那麼長的鼠標(biāo)來看下面的正題了\(^o^)/
測(cè)試類準(zhǔn)備
- 添加測(cè)試項(xiàng)目的引用

- 添加要模擬實(shí)現(xiàn)接口的Fakes程序集,要模擬的接口在Liuliu.Demo.Core程序集中,所以在該程序集上點(diǎn)右鍵,選擇“添加Fakes程序集”菜單項(xiàng)

- 添加好了之后,F(xiàn)akes框架會(huì)在測(cè)試項(xiàng)目中添加一個(gè)Fakes文件夾和一個(gè)配置文件,并自動(dòng)生成引用一個(gè) 模擬程序集.Fakes 的程序集和Fakes框架的運(yùn)行環(huán)境Microsoft.QualityTools.Testing.Fakes

- 打開對(duì)象查看器,可看到生成的Fakes程序集的內(nèi)容,所有的接口都生成了一個(gè)對(duì)應(yīng)的模擬類

- 通過ILSpy對(duì)Fakes程序集進(jìn)行反向,可以看到生成的模擬類如下所示,StubIMemberDao實(shí)現(xiàn)了接口IMemberDao,而接口中的公共成員都生成了“方法名+參數(shù)類型名”的委托模擬,用以接收外部給模擬方法的執(zhí)行結(jié)果賦值,這樣每個(gè)方法的返回值都可以被控制

- 另外生成的Fakes文件夾中的配置文件Liuliu.Demo.Core.fakes內(nèi)容如下所示
1 <Fakes xmlns="http://schemas.microsoft.com/fakes/2011/"> 2 <Assembly Name="Liuliu.Demo.Core"/> 3 </Fakes>
這個(gè)配置默認(rèn)會(huì)把測(cè)試程序集中的所有接口、類都生成模擬類,當(dāng)然也可以配置生成指定的類型的模擬,相關(guān)知識(shí)這里就不講了,請(qǐng)參閱官方文檔:Microsoft Fakes 中的代碼生成、編譯和命名約定
- 需要特別說明的是,每次生成,F(xiàn)akes程序集都會(huì)重新生成,所以測(cè)試類有更改后想刷新Fakes程序集,只需要把原來的程序集刪除再進(jìn)行生成,或者在測(cè)試項(xiàng)目能編譯的時(shí)候重新編譯測(cè)試項(xiàng)目即可。
TDD正式開始
- 給測(cè)試項(xiàng)目添加一個(gè)單元測(cè)試類文件,添加新項(xiàng) -> Visual C#項(xiàng) -> 測(cè)試 -> 單元測(cè)試,命名為AccountServiceTest.cs,推薦命名方式為“測(cè)試類名+Test”的方式
- 添加一個(gè)測(cè)試方法,關(guān)于測(cè)試方法的命名,各人有各人的方案,這里推薦一種方案:“測(cè)試方法名_執(zhí)行結(jié)果_得到此結(jié)果的條件/原因”,并且測(cè)試方法是可以使用中文的,比如“UserNameExistsCheck_用戶名已存在_用戶名在用戶信息表中已存在記錄”,這種方式好很多好處,特別是團(tuán)隊(duì)成員英文水平不太好的時(shí)候,如果翻譯成英文的方式,很有可能會(huì)不知所云,并且中文與需求文檔一一對(duì)應(yīng),非常明了,以下的測(cè)試用例中都會(huì)運(yùn)用這種方式,如果不適應(yīng)請(qǐng)?jiān)谀X中自行翻譯\(^o^)/,建立測(cè)試方法如下:
1 [TestMethod] 2 public void UserNameExistsCheck_用戶名不存在() 3 { 4 var userName = "柳柳英俠"; 5 var configName = "configName"; 6 var accountService = new AccountService(); 7 Assert.IsFalse(accountService.UserNameExistsCheck(userName, configName)); 8 }
當(dāng)然,此時(shí)運(yùn)行測(cè)試是編譯不過的,因?yàn)锳ccountService類根本還沒有創(chuàng)建。在Liuliu.Demo.Core.Business.Impl文件夾下添加AccountService類,并實(shí)現(xiàn)IAccountContract接口
1 /// <summary> 2 /// 賬戶模塊業(yè)務(wù)實(shí)現(xiàn)類 3 /// </summary> 4 public class AccountService : IAccountContract 5 { 6 /// <summary> 7 /// 用戶名重復(fù)檢查 8 /// </summary> 9 /// <param name="userName">用戶名</param> 10 /// <param name="configName">系統(tǒng)配置名稱</param> 11 /// <returns></returns> 12 public bool UserNameExistsCheck(string userName, string configName) 13 { 14 throw new NotImplementedException(); 15 } 16 17 /// <summary> 18 /// 電子郵箱重復(fù)檢查 19 /// </summary> 20 /// <param name="email">電子郵箱</param> 21 /// <param name="configName">系統(tǒng)配置名稱</param> 22 /// <returns></returns> 23 public bool EmailExistsCheck(string email, string configName) 24 { 25 throw new NotImplementedException(); 26 } 27 28 /// <summary> 29 /// 用戶注冊(cè) 30 /// </summary> 31 /// <param name="model">注冊(cè)信息模型</param> 32 /// <param name="configName">系統(tǒng)配置名稱</param> 33 /// <returns></returns> 34 public RegisterResults Register(Member model, string configName) 35 { 36 throw new NotImplementedException(); 37 } 38 }
再次運(yùn)行測(cè)試,是通不過,TDD的基本做法就是讓測(cè)試盡快通過,所以修改方法UserNameExistsCheck為如下:
1 /// <summary> 2 /// 用戶名重復(fù)檢查 3 /// </summary> 4 /// <param name="userName">用戶名</param> 5 /// <param name="configName">系統(tǒng)配置名稱</param> 6 /// <returns></returns> 7 public bool UserNameExistsCheck(string userName, string configName) 8 { 9 return false; 10 }
再次運(yùn)行測(cè)試用例,紅叉終于變成綠勾了,我敢打賭,如果你真正實(shí)踐TDD的話,綠色將是你一定會(huì)喜歡的顏色

參數(shù)的字符串,值的有效性一定要檢查的,所以添加以下兩個(gè)測(cè)試用例,通過ExpectedException特性可能確定拋出異常的類型1 [TestMethod] 2 [ExpectedException(typeof(ArgumentNullException))] 3 public void UserNameExistsCheck_引發(fā)ArgumentNullException異常_參數(shù)userName為空() 4 { 5 string userName = null; 6 var configName = "configName"; 7 var accountService = new AccountService(); 8 accountService.UserNameExistsCheck(userName, configName); 9 } 10 11 [TestMethod] 12 [ExpectedException(typeof(ArgumentNullException))] 13 public void UserNameExistsCheck_引發(fā)ArgumentNullException異常_參數(shù)configName為空() 14 { 15 var userName = "柳柳英俠"; 16 string configName = null; 17 var accountService = new AccountService(); 18 accountService.UserNameExistsCheck(userName, configName); 19 }
運(yùn)行測(cè)試,結(jié)果如下,原因?yàn)檫€沒有寫異常代碼,期望的異常沒有引發(fā)。└(^o^)┘平常我們很怕出異常,現(xiàn)在要去期望出異常

異常代碼編寫很簡(jiǎn)單,修改為如下即可通過: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 return false; 12 }
給AccountService類添加如下屬性,以便在接下來的操作中能模擬調(diào)用數(shù)據(jù)訪問層的操作
1 #region 屬性 2 3 /// <summary> 4 /// 獲取或設(shè)置 數(shù)據(jù)訪問對(duì)象——用戶信息 5 /// </summary> 6 public IMemberDao MemberDao { get; set; } 7 8 /// <summary> 9 /// 獲取或設(shè)置 數(shù)據(jù)訪問對(duì)象——未激活用戶信息 10 /// </summary> 11 public IMemberInactiveDao MemberInactiveDao { get; set; } 12 13 /// <summary> 14 /// 獲取或設(shè)置 數(shù)據(jù)訪問對(duì)象——系統(tǒng)配置信息 15 /// </summary> 16 public IConfigInfoDao ConfigInfoDao { get; set; } 17 18 #endregion
接下來該進(jìn)行用戶名存在的判斷了,即為在用戶信息數(shù)據(jù)庫中(MemberDao)存在相同用戶名的用戶信息,在這里的查詢實(shí)際并不是到數(shù)據(jù)庫中查詢,而是通過Fakes框架生成的模擬類模擬出一個(gè)查詢過程與獲得查詢結(jié)果。添加的測(cè)試用例如下:
1 [TestMethod] 2 public void UserNameExistsCheck_用戶名存在_該用戶名在用戶數(shù)據(jù)庫中已存在記錄() 3 { 4 var userName = "柳柳英俠"; 5 var configName = "configName"; 6 var accountService = new AccountService(); 7 var memberDao = new StubIMemberDao(); 8 memberDao.GetByNameString = str => new Member(); 9 accountService.MemberDao = memberDao; 10 Assert.IsTrue(accountService.UserNameExistsCheck(userName, configName)); 11 }
StubIMemberDao類即為Fakes框架由IMemberDao接口生成的一個(gè)模擬類,第7行實(shí)例化了一個(gè)該類的對(duì)象, 這個(gè)對(duì)象有一個(gè)委托類型的字段GetByNameString開放出來,我們就可以通過這個(gè)字段給接口的GetByName方法賦一個(gè)執(zhí)行結(jié)果,即第8行的操作。再把這個(gè)對(duì)象賦給AccountService類中的IMemberDao類型的屬性(第9行),即相當(dāng)于給AccountService類添加了一個(gè)操作用戶信息數(shù)據(jù)層的實(shí)現(xiàn)。
修改UserNameExistsCheck方法使測(cè)試通過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 return false; 17 }
運(yùn)行測(cè)試,上面這個(gè)測(cè)試通過了,但第一個(gè)測(cè)試卻失敗了。

這不合乎TDD的要求了,TDD要求后面添加的功能不能影響原來的功能。看代碼實(shí)現(xiàn)是沒有問題的,看來問題是出在測(cè)試用例上。
當(dāng)我們走到“UserNameExistsCheck_用戶名存在_該用戶名在用戶數(shù)據(jù)庫中已存在記錄”這個(gè)測(cè)試用例的時(shí)候,添加了一些屬性,而這些屬性在第一個(gè)測(cè)試用例“UserNameExistsCheck_用戶名不存在”并沒有進(jìn)行初始化,所以報(bào)了一個(gè)NullReferenceException異常。
接下來我們來優(yōu)化測(cè)試類的結(jié)構(gòu)來解決這些問題:
a. 每個(gè)測(cè)試用例的先決條件都要從0開始初始化,太麻煩
b. 測(cè)試環(huán)境沒有初始化,新增條件會(huì)影響到舊的測(cè)試用例的運(yùn)行 - 根據(jù)以上提出的問題,給出下面的解決方案
a. 進(jìn)行公共環(huán)境的初始化,即讓所有測(cè)試用例在相同的環(huán)境下運(yùn)行
b. 所有的模擬環(huán)境都初始化為“正確的”,結(jié)合現(xiàn)有場(chǎng)景,即認(rèn)為:數(shù)據(jù)訪問層的所有操作是可用的,并且能提供運(yùn)行結(jié)果的,即查詢能查到數(shù)據(jù),增刪改能操作成功。
c. 當(dāng)需要不正確的環(huán)境時(shí)再單獨(dú)進(jìn)行覆蓋設(shè)置(即重新給模擬方法的執(zhí)行結(jié)果賦值)
根據(jù)以上方案對(duì)測(cè)試類初始化為如下:給測(cè)試類添加字段和每個(gè)方法運(yùn)行前都運(yùn)行的公共方法
1 #region 字段 2 3 private readonly AccountService _accountService = new AccountService(); 4 private readonly StubIMemberDao _memberDao = new StubIMemberDao(); 5 private readonly StubIMemberInactiveDao _memberInactiveDao = new StubIMemberInactiveDao(); 6 private readonly StubIConfigInfoDao _configInfoDao = new StubIConfigInfoDao(); 7 8 private int _num = 1; 9 private Member _member = new Member(); 10 private readonly List<Member> _memberList = new List<Member>(); 11 private MemberInactive _memberInactive = new MemberInactive(); 12 private readonly List<MemberInactive> _memberInactiveList = new List<MemberInactive>(); 13 private ConfigInfo _configInfo = new ConfigInfo(); 14 15 #endregion
1 // 在運(yùn)行每個(gè)測(cè)試之前,使用 TestInitialize 來運(yùn)行代碼 2 [TestInitialize()] 3 public void MyTestInitialize() 4 { 5 _memberDao.Commit = () => _num; 6 _memberDao.DeleteMemberBoolean = (@member, @bool) => _num; 7 _memberDao.GetByEmailString = @string => _memberList; 8 _memberDao.GetByIdObject = @id => _member; 9 _memberDao.GetByNameString = @string => _member; 10 _memberDao.InsertMemberBoolean = (@member, @bool) => _num; 11 _accountService.MemberDao = _memberDao; 12 13 _memberInactiveDao.Commit = () => _num; 14 _memberInactiveDao.DeleteMemberInactiveBoolean = (@memberInactive, @bool) => _num; 15 _memberInactiveDao.GetByEmailString = @string => _memberInactiveList; 16 _memberInactiveDao.GetByIdObject = @id => _memberInactive; 17 _memberInactiveDao.GetByNameString = @string => _memberInactive; 18 _memberInactiveDao.InsertMemberInactiveBoolean = (@memberInactive, @bool) => _num; 19 _accountService.MemberInactiveDao = _memberInactiveDao; 20 21 _configInfoDao.Commit = () => _num; 22 _configInfoDao.DeleteConfigInfoBoolean = (@configInfo, @bool) => _num; 23 _configInfoDao.GetByIdObject = @id => _configInfo; 24 _configInfoDao.GetByNameString = @string => _configInfo; 25 _configInfoDao.InsertConfigInfoBoolean = (@configInfo, @bool) => _num; 26 _accountService.ConfigInfoDao = _configInfoDao; 27 28 }
有了初始化以后,原來的測(cè)試用例就可以如此的簡(jiǎn)單,只需要初始化不成立的條件即可
1 #region UserNameExistsCheck 2 [TestMethod] 3 public void UserNameExistsCheck_用戶名不存在() 4 { 5 var userName = "柳柳英俠"; 6 var configName = "configName"; 7 _member = null; 8 Assert.IsFalse(_accountService.UserNameExistsCheck(userName, configName)); 9 } 10 11 [TestMethod] 12 [ExpectedException(typeof(ArgumentNullException))] 13 public void UserNameExistsCheck_引發(fā)ArgumentNullException異常_參數(shù)userName為空() 14 { 15 string userName = null; 16 var configName = "configName"; 17 _accountService.UserNameExistsCheck(userName, configName); 18 } 19 20 [TestMethod] 21 [ExpectedException(typeof(ArgumentNullException))] 22 public void UserNameExistsCheck_引發(fā)ArgumentNullException異常_參數(shù)configName為空() 23 { 24 var userName = "柳柳英俠"; 25 string configName = null; 26 _accountService.UserNameExistsCheck(userName, configName); 27 } 28 29 [TestMethod] 30 public void UserNameExistsCheck_用戶名存在_該用戶名在用戶數(shù)據(jù)庫中已存在記錄() 31 { 32 var userName = "柳柳英俠"; 33 var configName = "configName"; 34 Assert.IsTrue(_accountService.UserNameExistsCheck(userName, configName)); 35 } 36 37 #endregion
所有條件都初始化好了,繼續(xù)研究需求,就可以把測(cè)試用例的所有情況都寫出來
1 [TestMethod] 2 [ExpectedException(typeof(NullReferenceException))] 3 public void UserNameExistsCheck_引發(fā)NullReferenceException異常_系統(tǒng)配置信息無法找到() 4 { 5 var userName = "柳柳英俠"; 6 var configName = "configName"; 7 _member = null; 8 _configInfo = null; 9 _accountService.UserNameExistsCheck(userName, configName); 10 } 11 12 [TestMethod] 13 public void UserNameExistsCheck_用戶不存在_用戶在用戶數(shù)據(jù)庫中不存在_and_注冊(cè)不需要激活() 14 { 15 var userName = "柳柳英俠"; 16 var configName = "configName"; 17 _member = null; 18 _configInfo.RegisterConfig.NeedActive = false; 19 Assert.IsFalse(_accountService.UserNameExistsCheck(userName, configName)); 20 } 21 22 [TestMethod] 23 public void UserNameExistsCheck_用戶不存在_用戶在用戶數(shù)據(jù)庫中不存在_and_注冊(cè)需要激活_and_用戶名在未激活用戶數(shù)據(jù)庫中不存在() 24 { 25 var userName = "柳柳英俠"; 26 var configName = "configName"; 27 _member = null; 28 _configInfo.RegisterConfig.NeedActive = true; 29 _memberInactive = null; 30 Assert.IsFalse(_accountService.UserNameExistsCheck(userName, configName)); 31 }
編寫代碼讓測(cè)試通過
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) 27 { 28 return true; 29 } 30 return false; 31 }

總結(jié)
看起來文章寫得挺長了,其實(shí)內(nèi)容并沒有多少,篇幅都被代碼拉開了。我們來總結(jié)一下使用Fakes框架進(jìn)行TDD開發(fā)的步驟:
- 建立底層接口
- 創(chuàng)建測(cè)試接口的Fakes程序集
- 創(chuàng)建環(huán)境完全初始化的測(cè)試類(這點(diǎn)比較麻煩,可以配合T4模板進(jìn)行生成)
- 分析需求寫測(cè)試用例
- 編寫代碼讓測(cè)試用例通過
- 重構(gòu)代碼,并保證重構(gòu)的代碼仍然能讓測(cè)試用例通過
另外有幾點(diǎn)經(jīng)驗(yàn)之談:
- 測(cè)試用例的方法名完全可以包含中文,清晰明了
- 由于測(cè)試類的環(huán)境已完全初始化,可以根據(jù)需求把所有的測(cè)試用例一次寫出來,不確定的可以留為空方法,也不會(huì)影響測(cè)試通過
- 當(dāng)你習(xí)慣了TDD之后,你會(huì)離不開它的└(^o^)┘
本篇只對(duì)底層的接口進(jìn)行了模擬,在下篇將對(duì)測(cè)試類中的私有方法,靜態(tài)方法等進(jìn)行模擬,敬請(qǐng)期待^_^o~ 努力!
源碼下載
參考資料
1.Microsoft Fakes 中的代碼生成、編譯和命名約定:
http://msdn.microsoft.com/zh-cn/library/hh708916
2.使用存根隔離對(duì)單元測(cè)試方法中虛擬函數(shù)的調(diào)用
http://msdn.microsoft.com/zh-cn/library/hh549174
3.使用填充碼隔離對(duì)單元測(cè)試方法中非虛擬函數(shù)的調(diào)用
http://msdn.microsoft.com/zh-cn/library/hh549176
作者:郭明鋒
Q群:MVC EF技術(shù)交流(5008599)
OSharp開發(fā)框架交流(85895249)
出處:http://www.rzrgm.cn/guomingfeng
聲明:本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。


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