Visual Studio 2012 Fakes框架測試驅動開發TDD教程

 

1、前言

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

2、需求說明

  咱們要作的是一個普通的用戶註冊中「檢查用戶名是否存在」的功能,需求以下:數據結構

  1. 用戶名不能重複
  2. 可設置是否啓用郵件激活,若是不啓用郵件激活,則直接在「正式用戶信息表」中檢查,反之則還要進入「未激活用戶信息表」中進行查詢

3、項目結構

【Visual Studio 2012 Fakes框架測試驅動開發TDD教程】一、接口模擬_www.fengfly.com

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

  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項目

  其餘的項目與測試無關,略過。函數

4、開發準備

(一) 應用代碼準備

Entity:實體類的通用數據結構工具

  1. /// <summary>  
  2.      ///   數據實體類基類,定義數據庫存儲的數據結構的通用部分  
  3.      /// </summary>  
  4.      public abstract class Entity  
  5.      {  
  6.          /// <summary>  
  7.          ///   編號  
  8.          /// </summary>  
  9.          public int Id { getset; }  
  10.    
  11.          /// <summary>  
  12.          ///   是否邏輯刪除(至關於回收站,非物理刪除)  
  13.          /// </summary>  
  14.          public bool IsDelete { getset; }  
  15.    
  16.          /// <summary>  
  17.          ///   添加時間  
  18.          /// </summary>  
  19.          public DateTime AddDate { getset; }  
  20.      } 
  21. 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 { getset; }  
    7.    
    8.          public string Password { getset; }  
    9.    
    10.          public string Email { getset; }  
    11.      } 

    MemberInactive:實體類——未激活用戶信息優化

    1. /// <summary>  
    2.      ///   實體類——未激活用戶信息  
    3.      /// </summary>  
    4.      public class MemberInactive : Entity  
    5.      {  
    6.          public string UserName { getset; }  
    7.    
    8.          public string Password { getset; }  
    9.    
    10.          public string Email { getset; }  
    11.      } 

    ConfigInfo:實體類——系統配置信息spa

    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 { getset; }  
    12.      }  
    13.    
    14.    
    15.      public class RegisterConfig  
    16.      {  
    17.          /// <summary>  
    18.          ///   註冊時是否須要Email激活  
    19.          /// </summary>  
    20.          public bool NeedActive { getset; }  
    21.    
    22.          /// <summary>  
    23.          ///   激活郵件有效期,單位:分鐘  
    24.          /// </summary>  
    25.          public int ActiveTimeout { getset; }  
    26.    
    27.          /// <summary>  
    28.          ///   容許同一Email註冊不一樣會員  
    29.          /// </summary>  
    30.          public bool EmailRepeat { getset; }  
    31.      } 
  22. 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. 添加測試項目的引用

    【Visual Studio 2012 Fakes框架測試驅動開發TDD教程】一、接口模擬(4)_www.fengfly.com

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

    【Visual Studio 2012 Fakes框架測試驅動開發TDD教程】一、接口模擬(4)_www.fengfly.com

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

    【Visual Studio 2012 Fakes框架測試驅動開發TDD教程】一、接口模擬(4)_www.fengfly.com

  4. 打開對象查看器,可看到生成的Fakes程序集的內容,全部的接口都生成了一個對應的模擬類
    【Visual Studio 2012 Fakes框架測試驅動開發TDD教程】一、接口模擬(4)_www.fengfly.com
  5. 經過ILSpy對Fakes程序集進行反向,能夠看到生成的模擬類以下所示,StubIMemberDao實現了接口IMemberDao,而接口中的公共成員都生成了「方法名+參數類型名」的委託模擬,用以接收外部給模擬方法的執行結果賦值,這樣每一個方法的返回值均可以被控制【Visual Studio 2012 Fakes框架測試驅動開發TDD教程】一、接口模擬(4)_www.fengfly.com
  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的話,綠色將是你必定會喜歡的顏色

【Visual Studio 2012 Fakes框架測試驅動開發TDD教程】一、接口模擬(5)_www.fengfly.com
參數的字符串,值的有效性必定要檢查的,因此添加如下兩個測試用例,經過ExpectedException特性可能肯定拋出異常的類型

    1. 運行測試,結果以下,緣由爲尚未寫異常代碼,指望的異常沒有引起。└(^o^)┘日常咱們很怕出異常,如今要去指望出異常

      【Visual Studio 2012 Fakes框架測試驅動開發TDD教程】一、接口模擬(6)_www.fengfly.com
      異常代碼編寫很簡單,修改成以下便可經過:

      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 { getset; }  
      7.    
      8.          /// <summary>  
      9.          /// 獲取或設置 數據訪問對象——未激活用戶信息  
      10.          /// </summary>  
      11.          public IMemberInactiveDao MemberInactiveDao { getset; }  
      12.    
      13.          /// <summary>  
      14.          /// 獲取或設置 數據訪問對象——系統配置信息  
      15.          /// </summary>  
      16.          public IConfigInfoDao ConfigInfoDao { getset; }  
      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.          } 

      運行測試,上面這個測試經過了,但第一個測試卻失敗了。

      【Visual Studio 2012 Fakes框架測試驅動開發TDD教程】一、接口模擬(6)_www.fengfly.com
      這不合乎TDD的要求了,TDD要求後面添加的功能不能影響原來的功能。看代碼實現是沒有問題的,看來問題是出在測試用例上。
      當咱們走到「UserNameExistsCheck_用戶名存在_該用戶名在用戶數據庫中已存在記錄」這個測試用例的時候,添加了一些屬性,而這些屬性在第一個測試用例「UserNameExistsCheck_用戶名不存在」並無進行初始化,因此報了一個NullReferenceException異常。

      接下來咱們來優化測試類的結構來解決這些問題:
      a. 每一個測試用例的先決條件都要從0開始初始化,太麻煩
      b. 測試環境沒有初始化,新增條件會影響到舊的測試用例的運行

    2. 根據以上提出的問題,給出下面的解決方案
      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 
      [TestMethod]  
    3.          [ExpectedException(typeof(ArgumentNullException))]  
    4.          public void UserNameExistsCheck_引起ArgumentNullException異常_參數userName爲空()  
    5.          {  
    6.              string userName = null;  
    7.              var configName = "configName";  
    8.              var accountService = new AccountService();  
    9.              accountService.UserNameExistsCheck(userName, configName);  
    10.          }  
    11.    
    12.          [TestMethod]  
    13.          [ExpectedException(typeof(ArgumentNullException))]  
    14.          public void UserNameExistsCheck_引起ArgumentNullException異常_參數configName爲空()  
    15.          {  
    16.              var userName = "柳柳英俠";  
    17.              string configName = null;  
    18.              var accountService = new AccountService();  
    19.              accountService.UserNameExistsCheck(userName, configName);  
    20.          } 
  1. 全部條件都初始化好了,繼續研究需求,就能夠把測試用例的全部狀況都寫出來

    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.          } 

     【Visual Studio 2012 Fakes框架測試驅動開發TDD教程】一、接口模擬(8)_www.fengfly.com

5、總結

  看起來文章寫得挺長了,其實內容並無多少,篇幅都被代碼拉開了。咱們來總結一下使用Fakes框架進行TDD開發的步驟:

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

  另外有幾點經驗之談:

  1. 測試用例的方法名徹底能夠包含中文,清晰明瞭
  2. 因爲測試類的環境已徹底初始化,能夠根據需求把全部的測試用例一次寫出來,不肯定的能夠留爲空方法,也不會影響測試經過
  3. 當你習慣了TDD以後,你會離不開它的└(^o^)┘

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

6、源碼下載

LiuliuTDDFakesDemo01.rar

7、參考資料

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

相關文章
相關標籤/搜索