前言
最近團隊要嘗試TDD(測試驅動開發)的實踐,很多人習慣了先代碼后測試的流程,對於TDD總心存恐懼,認為沒有代碼的情況下寫測試代碼時被架空了,沒法寫下來,其實,根據個人實踐經驗,TDD並不可怕,還很可愛,只要你真正去實踐了幾十個測試用例之后,你會愛上這種開發方式的。微軟對於TDD的開發方式是大力支持和推薦的,新發布的VS2012的團隊模板就是根據。新的Visual Studio 2012給我們帶來了Fakes框架,這是一個針對代碼測試時對測試的外界依賴(如數據庫,文件等)進行模擬的Mock框架,用上了之后,我立即從Moq的陣營中叛變了^_^。截止到寫此文的時間,網上還沒有一篇關於Fakes框架的文章(除了“VS11將擁有更好的單元測試工具和Fakes框架”這篇介紹性的之外),就讓我們來慢慢摸索着用吧。廢話少說,下面我們就來一步一步的使用Visual Studio 2012的Fakes框架來實戰一把TDD。
需求說明
我們要做的是一個普通的用戶注冊中“檢查用戶名是否存在”的功能,需求如下:
- 用戶名不能重復
- 可設置是否啟用郵件激活,如果不啟用郵件激活,則直接在“正式用戶信息表”中檢查,反之則還要進入“未激活用戶信息表”中進行查詢
項目結構
先分解一下項目的結構,還是傳統的三層結構,從底層到上層:
- Liuliu.Components.Tools:通用工具組件
- Liuliu.Components.Data:通用數據訪問組件,目前只定義了一個數據訪問接口的通用基接口IRepository
- Liuliu.Demo.Core.Models:數據實體類,分兩個模塊,賬戶模塊(Account)與通用模塊(Common)
- Liuliu.Demo.Core:業務核心層,里面包含Business與DataAccess兩個子層,DataAccess實現實體類的數據訪問,Business層實現模塊的業務邏輯,因為測試的過程中數據訪問層的數據庫實現會用Fakes框架來模擬,所以數據訪問層只提供了接口,不提供實現,Business只調用了DataAccess的接口。我們要做的工作就是用Fakes框架來模擬數據訪問層,用TDD的方式來編寫Business中的業務實現
- Liuliu.Demo.Core.Business.UnitTest:單元測試項目,存放着測試Business實現的測試用例。
- Liuliu.Demo.Consoles:用戶操作控制台,功能實現后進行用戶操作的UI項目
其他的項目與測試無關,略過。
開發准備
應用代碼准備
Entity:實體類的通用數據結構
1 /// <summary> 2 /// 數據實體類基類,定義數據庫存儲的數據結構的通用部分 3 /// </summary> 4 public abstract class Entity 5 { 6 /// <summary> 7 /// 編號 8 /// </summary> 9 public int Id { get; set; } 10 11 /// <summary> 12 /// 是否邏輯刪除(相當於回收站,非物理刪除) 13 /// </summary> 14 public bool IsDelete { get; set; } 15 16 /// <summary> 17 /// 添加時間 18 /// </summary> 19 public DateTime AddDate { get; set; } 20 }
IRepository:通用數據訪問接口,簡單起見,只寫了幾個增刪改查的接口
1 /// <summary> 2 /// 定義倉儲模式中的數據標准操作,其實現類是倉儲類型。 3 /// </summary> 4 /// <typeparam name="TEntity">要實現倉儲的類型</typeparam> 5 public interface IRepository<TEntity> where TEntity : Entity 6 { 7 #region 公用方法 8 9 /// <summary> 10 /// 插入實體記錄 11 /// </summary> 12 /// <param name="entity"> 實體對象 </param> 13 /// <param name="isSave"> 是否執行保存 </param> 14 /// <returns> 操作影響的行數 </returns> 15 int Insert(TEntity entity, bool isSave = true); 16 17 /// <summary> 18 /// 刪除實體記錄 19 /// </summary> 20 /// <param name="entity"> 實體對象 </param> 21 /// <param name="isSave"> 是否執行保存 </param> 22 /// <returns> 操作影響的行數 </returns> 23 int Delete(TEntity entity, bool isSave = true); 24 25 /// <summary> 26 /// 更新實體記錄 27 /// </summary> 28 /// <param name="entity"> 實體對象 </param> 29 /// <param name="isSave"> 是否執行保存 </param> 30 /// <returns> 操作影響的行數 </returns> 31 int Update(TEntity entity, bool isSave = true); 32 33 /// <summary> 34 /// 提交當前的Unit Of Work事務,作用與 IUnitOfWork.Commit() 相同。 35 /// </summary> 36 /// <returns>提交事務影響的行數</returns> 37 int Commit(); 38 39 /// <summary> 40 /// 查找指定編號的實體記錄 41 /// </summary> 42 /// <param name="id"> 指定編號 </param> 43 /// <returns> 符合編號的記錄,不存在返回null </returns> 44 TEntity GetById(object id); 45 46 /// <summary> 47 /// 查找指定名稱的實體記錄,注意:如實體無名稱屬性則不支持 48 /// </summary> 49 /// <param name="name">名稱</param> 50 /// <returns>符合名稱的記錄,不存在則返回null</returns> 51 /// <exception cref="NotSupportedException">當對應實體無名稱時引發將引發異常</exception> 52 TEntity GetByName(string name); 53 54 #endregion 55 }
Member:實體類——用戶信息
1 /// <summary> 2 /// 實體類——用戶信息 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:實體類——未激活用戶信息
1 /// <summary> 2 /// 實體類——未激活用戶信息 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:實體類——系統配置信息
1 /// <summary> 2 /// 實體類——系統配置信息 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 /// 注冊時是否需要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注冊不同會員 29 /// </summary> 30 public bool EmailRepeat { get; set; } 31 }
IMemberDao:數據訪問接口——用戶信息,僅添加IRepository不滿足的接口
1 /// <summary> 2 /// 數據訪問接口——用戶信息 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:數據訪問接口——未激活用戶信息,僅添加IRepository不滿足的接口
1 /// <summary> 2 /// 數據訪問接口——未激活用戶信息 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:數據訪問接口——系統配置,無額外需求的接口,所以為空接口
1 /// <summary> 2 /// 數據訪問接口——系統配置信息 3 /// </summary> 4 public interface IConfigInfoDao : IRepository<ConfigInfo> 5 { }
IAccountContract:賬戶模塊業務契約——定義了三個操作,用作注冊前的數據檢查和注冊提交
1 /// <summary> 2 /// 核心業務契約——賬戶模塊 3 /// </summary> 4 public interface IAccountContract 5 { 6 /// <summary> 7 /// 用戶名重復檢查 8 /// </summary> 9 /// <param name="userName">用戶名</param> 10 /// <param name="configName">系統配置名稱</param> 11 /// <returns></returns> 12 bool UserNameExistsCheck(string userName, string configName); 13 14 /// <summary> 15 /// 電子郵箱重復檢查 16 /// </summary> 17 /// <param name="email">電子郵箱</param> 18 /// <param name="configName">系統配置名稱</param> 19 /// <returns></returns> 20 bool EmailExistsCheck(string email, string configName); 21 22 /// <summary> 23 /// 用戶注冊 24 /// </summary> 25 /// <param name="model">注冊信息模型</param> 26 /// <param name="configName">系統配置名稱</param> 27 /// <returns></returns> 28 RegisterResults Register(Member model, string configName); 29 }
以上代碼本來想收起來的,但測試時代碼展開老失效,所以辛苦大家划了那麽長的鼠標來看下面的正題了\(^o^)/
測試類准備
- 添加測試項目的引用
- 添加要模擬實現接口的Fakes程序集,要模擬的接口在Liuliu.Demo.Core程序集中,所以在該程序集上點右鍵,選擇“添加Fakes程序集”菜單項
- 添加好了之后,Fakes框架會在測試項目中添加一個Fakes文件夾和一個配置文件,並自動生成引用一個 模擬程序集.Fakes 的程序集和Fakes框架的運行環境Microsoft.QualityTools.Testing.Fakes
- 打開對象查看器,可看到生成的Fakes程序集的內容,所有的接口都生成了一個對應的模擬類
- 通過ILSpy對Fakes程序集進行反向,可以看到生成的模擬類如下所示,StubIMemberDao實現了接口IMemberDao,而接口中的公共成員都生成了“方法名+參數類型名”的委托模擬,用以接收外部給模擬方法的執行結果賦值,這樣每個方法的返回值都可以被控制
- 另外生成的Fakes文件夾中的配置文件Liuliu.Demo.Core.fakes內容如下所示
1 <Fakes xmlns="http://schemas.microsoft.com/fakes/2011/"> 2 <Assembly Name="Liuliu.Demo.Core"/> 3 </Fakes>
這個配置默認會把測試程序集中的所有接口、類都生成模擬類,當然也可以配置生成指定的類型的模擬,相關知識這里就不講了,請參閱官方文檔:Microsoft Fakes 中的代碼生成、編譯和命名約定
- 需要特別說明的是,每次生成,Fakes程序集都會重新生成,所以測試類有更改后想刷新Fakes程序集,只需要把原來的程序集刪除再進行生成,或者在測試項目能編譯的時候重新編譯測試項目即可。
TDD正式開始
- 給測試項目添加一個單元測試類文件,添加新項 -> Visual C#項 -> 測試 -> 單元測試,命名為AccountServiceTest.cs,推薦命名方式為“測試類名+Test”的方式
- 添加一個測試方法,關於測試方法的命名,各人有各人的方案,這里推薦一種方案:“測試方法名_執行結果_得到此結果的條件/原因”,並且測試方法是可以使用中文的,比如“UserNameExistsCheck_用戶名已存在_用戶名在用戶信息表中已存在記錄”,這種方式好很多好處,特別是團隊成員英文水平不太好的時候,如果翻譯成英文的方式,很有可能會不知所雲,並且中文與需求文檔一一對應,非常明了,以下的測試用例中都會運用這種方式,如果不適應請在腦中自行翻譯\(^o^)/,建立測試方法如下:
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 }
當然,此時運行測試是編譯不過的,因為AccountService類根本還沒有創建。在Liuliu.Demo.Core.Business.Impl文件夾下添加AccountService類,並實現IAccountContract接口
1 /// <summary> 2 /// 賬戶模塊業務實現類 3 /// </summary> 4 public class AccountService : IAccountContract 5 { 6 /// <summary> 7 /// 用戶名重復檢查 8 /// </summary> 9 /// <param name="userName">用戶名</param> 10 /// <param name="configName">系統配置名稱</param> 11 /// <returns></returns> 12 public bool UserNameExistsCheck(string userName, string configName) 13 { 14 throw new NotImplementedException(); 15 } 16 17 /// <summary> 18 /// 電子郵箱重復檢查 19 /// </summary> 20 /// <param name="email">電子郵箱</param> 21 /// <param name="configName">系統配置名稱</param> 22 /// <returns></returns> 23 public bool EmailExistsCheck(string email, string configName) 24 { 25 throw new NotImplementedException(); 26 } 27 28 /// <summary> 29 /// 用戶注冊 30 /// </summary> 31 /// <param name="model">注冊信息模型</param> 32 /// <param name="configName">系統配置名稱</param> 33 /// <returns></returns> 34 public RegisterResults Register(Member model, string configName) 35 { 36 throw new NotImplementedException(); 37 } 38 }
再次運行測試,是通不過,TDD的基本做法就是讓測試盡快通過,所以修改方法UserNameExistsCheck為如下:
1 /// <summary> 2 /// 用戶名重復檢查 3 /// </summary> 4 /// <param name="userName">用戶名</param> 5 /// <param name="configName">系統配置名稱</param> 6 /// <returns></returns> 7 public bool UserNameExistsCheck(string userName, string configName) 8 { 9 return false; 10 }
再次運行測試用例,紅叉終於變成綠勾了,我敢打賭,如果你真正實踐TDD的話,綠色將是你一定會喜歡的顏色
參數的字符串,值的有效性一定要檢查的,所以添加以下兩個測試用例,通過ExpectedException特性可能確定拋出異常的類型1 [TestMethod] 2 [ExpectedException(typeof(ArgumentNullException))] 3 public void UserNameExistsCheck_引發ArgumentNullException異常_參數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_引發ArgumentNullException異常_參數configName為空() 14 { 15 var userName = "柳柳英俠"; 16 string configName = null; 17 var accountService = new AccountService(); 18 accountService.UserNameExistsCheck(userName, configName); 19 }
運行測試,結果如下,原因為還沒有寫異常代碼,期望的異常沒有引發。└(^o^)┘平常我們很怕出異常,現在要去期望出異常
異常代碼編寫很簡單,修改為如下即可通過: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類添加如下屬性,以便在接下來的操作中能模擬調用數據訪問層的操作
1 #region 屬性 2 3 /// <summary> 4 /// 獲取或設置 數據訪問對象——用戶信息 5 /// </summary> 6 public IMemberDao MemberDao { get; set; } 7 8 /// <summary> 9 /// 獲取或設置 數據訪問對象——未激活用戶信息 10 /// </summary> 11 public IMemberInactiveDao MemberInactiveDao { get; set; } 12 13 /// <summary> 14 /// 獲取或設置 數據訪問對象——系統配置信息 15 /// </summary> 16 public IConfigInfoDao ConfigInfoDao { get; set; } 17 18 #endregion
接下來該進行用戶名存在的判斷了,即為在用戶信息數據庫中(MemberDao)存在相同用戶名的用戶信息,在這里的查詢實際並不是到數據庫中查詢,而是通過Fakes框架生成的模擬類模擬出一個查詢過程與獲得查詢結果。添加的測試用例如下:
1 [TestMethod] 2 public void UserNameExistsCheck_用戶名存在_該用戶名在用戶數據庫中已存在記錄() 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接口生成的一個模擬類,第7行實例化了一個該類的對象, 這個對象有一個委托類型的字段GetByNameString開放出來,我們就可以通過這個字段給接口的GetByName方法賦一個執行結果,即第8行的操作。再把這個對象賦給AccountService類中的IMemberDao類型的屬性(第9行),即相當於給AccountService類添加了一個操作用戶信息數據層的實現。
修改UserNameExistsCheck方法使測試通過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 }
運行測試,上面這個測試通過了,但第一個測試卻失敗了。
這不合乎TDD的要求了,TDD要求后面添加的功能不能影響原來的功能。看代碼實現是沒有問題的,看來問題是出在測試用例上。
當我們走到“UserNameExistsCheck_用戶名存在_該用戶名在用戶數據庫中已存在記錄”這個測試用例的時候,添加了一些屬性,而這些屬性在第一個測試用例“UserNameExistsCheck_用戶名不存在”並沒有進行初始化,所以報了一個NullReferenceException異常。
接下來我們來優化測試類的結構來解決這些問題:
a. 每個測試用例的先決條件都要從0開始初始化,太麻煩
b. 測試環境沒有初始化,新增條件會影響到舊的測試用例的運行 - 根據以上提出的問題,給出下面的解決方案
a. 進行公共環境的初始化,即讓所有測試用例在相同的環境下運行
b. 所有的模擬環境都初始化為“正確的”,結合現有場景,即認為:數據訪問層的所有操作是可用的,並且能提供運行結果的,即查詢能查到數據,增刪改能操作成功。
c. 當需要不正確的環境時再單獨進行覆蓋設置(即重新給模擬方法的執行結果賦值)
根據以上方案對測試類初始化為如下:給測試類添加字段和每個方法運行前都運行的公共方法
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 // 在運行每個測試之前,使用 TestInitialize 來運行代碼 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 }
有了初始化以后,原來的測試用例就可以如此的簡單,只需要初始化不成立的條件即可
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_引發ArgumentNullException異常_參數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_引發ArgumentNullException異常_參數configName為空() 23 { 24 var userName = "柳柳英俠"; 25 string configName = null; 26 _accountService.UserNameExistsCheck(userName, configName); 27 } 28 29 [TestMethod] 30 public void UserNameExistsCheck_用戶名存在_該用戶名在用戶數據庫中已存在記錄() 31 { 32 var userName = "柳柳英俠"; 33 var configName = "configName"; 34 Assert.IsTrue(_accountService.UserNameExistsCheck(userName, configName)); 35 } 36 37 #endregion
所有條件都初始化好了,繼續研究需求,就可以把測試用例的所有情況都寫出來
1 [TestMethod] 2 [ExpectedException(typeof(NullReferenceException))] 3 public void UserNameExistsCheck_引發NullReferenceException異常_系統配置信息無法找到() 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_用戶不存在_用戶在用戶數據庫中不存在_and_注冊不需要激活() 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_用戶不存在_用戶在用戶數據庫中不存在_and_注冊需要激活_and_用戶名在未激活用戶數據庫中不存在() 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 }
編寫代碼讓測試通過
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) 27 { 28 return true; 29 } 30 return false; 31 }
總結
看起來文章寫得挺長了,其實內容並沒有多少,篇幅都被代碼拉開了。我們來總結一下使用Fakes框架進行TDD開發的步驟:
- 建立底層接口
- 創建測試接口的Fakes程序集
- 創建環境完全初始化的測試類(這點比較麻煩,可以配合T4模板進行生成)
- 分析需求寫測試用例
- 編寫代碼讓測試用例通過
- 重構代碼,並保證重構的代碼仍然能讓測試用例通過
另外有幾點經驗之談:
- 測試用例的方法名完全可以包含中文,清晰明了
- 由於測試類的環境已完全初始化,可以根據需求把所有的測試用例一次寫出來,不確定的可以留為空方法,也不會影響測試通過
- 當你習慣了TDD之后,你會離不開它的└(^o^)┘
本篇只對底層的接口進行了模擬,在下篇將對測試類中的私有方法,靜態方法等進行模擬,敬請期待^_^o~ 努力!
源碼下載
參考資料
1.Microsoft Fakes 中的代碼生成、編譯和命名約定:
http://msdn.microsoft.com/zh-cn/library/hh708916
2.使用存根隔離對單元測試方法中虛擬函數的調用
http://msdn.microsoft.com/zh-cn/library/hh549174
3.使用填充碼隔離對單元測試方法中非虛擬函數的調用
http://msdn.microsoft.com/zh-cn/library/hh549176