基於VS2012 Fakes框架的TDD實戰——接口模擬


前言

  最近團隊要嘗試TDD(測試驅動開發)的實踐,很多人習慣了先代碼后測試的流程,對於TDD總心存恐懼,認為沒有代碼的情況下寫測試代碼時被架空了,沒法寫下來,其實,根據個人實踐經驗,TDD並不可怕,還很可愛,只要你真正去實踐了幾十個測試用例之后,你會愛上這種開發方式的。微軟對於TDD的開發方式是大力支持和推薦的,新發布的VS2012的團隊模板就是根據。新的Visual Studio 2012給我們帶來了Fakes框架,這是一個針對代碼測試時對測試的外界依賴(如數據庫,文件等)進行模擬的Mock框架,用上了之后,我立即從Moq的陣營中叛變了^_^。截止到寫此文的時間,網上還沒有一篇關於Fakes框架的文章(除了“VS11將擁有更好的單元測試工具和Fakes框架”這篇介紹性的之外),就讓我們來慢慢摸索着用吧。廢話少說,下面我們就來一步一步的使用Visual Studio 2012的Fakes框架來實戰一把TDD。

需求說明

  我們要做的是一個普通的用戶注冊中“檢查用戶名是否存在”的功能,需求如下:

  1. 用戶名不能重復
  2. 可設置是否啟用郵件激活,如果不啟用郵件激活,則直接在“正式用戶信息表”中檢查,反之則還要進入“未激活用戶信息表”中進行查詢

項目結構

  先分解一下項目的結構,還是傳統的三層結構,從底層到上層:

  1. Liuliu.Components.Tools:通用工具組件
  2. Liuliu.Components.Data:通用數據訪問組件,目前只定義了一個數據訪問接口的通用基接口IRepository
  3. Liuliu.Demo.Core.Models:數據實體類,分兩個模塊,賬戶模塊(Account)與通用模塊(Common)
  4. Liuliu.Demo.Core:業務核心層,里面包含Business與DataAccess兩個子層,DataAccess實現實體類的數據訪問,Business層實現模塊的業務邏輯,因為測試的過程中數據訪問層的數據庫實現會用Fakes框架來模擬,所以數據訪問層只提供了接口,不提供實現,Business只調用了DataAccess的接口。我們要做的工作就是用Fakes框架來模擬數據訪問層,用TDD的方式來編寫Business中的業務實現
  5. Liuliu.Demo.Core.Business.UnitTest:單元測試項目,存放着測試Business實現的測試用例。
  6. 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^)/

測試類准備

  1. 添加測試項目的引用

  2. 添加要模擬實現接口的Fakes程序集,要模擬的接口在Liuliu.Demo.Core程序集中,所以在該程序集上點右鍵,選擇“添加Fakes程序集”菜單項

  3. 添加好了之后,Fakes框架會在測試項目中添加一個Fakes文件夾和一個配置文件,並自動生成引用一個 模擬程序集.Fakes 的程序集和Fakes框架的運行環境Microsoft.QualityTools.Testing.Fakes

  4. 打開對象查看器,可看到生成的Fakes程序集的內容,所有的接口都生成了一個對應的模擬類
     
  5. 通過ILSpy對Fakes程序集進行反向,可以看到生成的模擬類如下所示,StubIMemberDao實現了接口IMemberDao,而接口中的公共成員都生成了“方法名+參數類型名”的委托模擬,用以接收外部給模擬方法的執行結果賦值,這樣每個方法的返回值都可以被控制
  6. 另外生成的Fakes文件夾中的配置文件Liuliu.Demo.Core.fakes內容如下所示
    1 <Fakes xmlns="http://schemas.microsoft.com/fakes/2011/">
    2   <Assembly Name="Liuliu.Demo.Core"/>
    3 </Fakes>

     這個配置默認會把測試程序集中的所有接口、類都生成模擬類,當然也可以配置生成指定的類型的模擬,相關知識這里就不講了,請參閱官方文檔:Microsoft Fakes 中的代碼生成、編譯和命名約定

  7. 需要特別說明的是,每次生成,Fakes程序集都會重新生成,所以測試類有更改后想刷新Fakes程序集,只需要把原來的程序集刪除再進行生成,或者在測試項目能編譯的時候重新編譯測試項目即可。

TDD正式開始

  1. 給測試項目添加一個單元測試類文件,添加新項 -> Visual C#項 -> 測試 -> 單元測試,命名為AccountServiceTest.cs,推薦命名方式為“測試類名+Test”的方式
  2. 添加一個測試方法,關於測試方法的命名,各人有各人的方案,這里推薦一種方案:“測試方法名_執行結果_得到此結果的條件/原因”,並且測試方法是可以使用中文的,比如“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. 測試環境沒有初始化,新增條件會影響到舊的測試用例的運行

  3. 根據以上提出的問題,給出下面的解決方案
    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開發的步驟:

  1. 建立底層接口
  2. 創建測試接口的Fakes程序集
  3. 創建環境完全初始化的測試類(這點比較麻煩,可以配合T4模板進行生成)
  4. 分析需求寫測試用例
  5. 編寫代碼讓測試用例通過
  6. 重構代碼,並保證重構的代碼仍然能讓測試用例通過

  另外有幾點經驗之談:

  1. 測試用例的方法名完全可以包含中文,清晰明了
  2. 由於測試類的環境已完全初始化,可以根據需求把所有的測試用例一次寫出來,不確定的可以留為空方法,也不會影響測試通過
  3. 當你習慣了TDD之后,你會離不開它的└(^o^)┘

本篇只對底層的接口進行了模擬,在下篇將對測試類中的私有方法,靜態方法等進行模擬,敬請期待^_^o~ 努力!

源碼下載

LiuliuTDDFakesDemo01.rar

參考資料

 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

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM