好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

基于VS2012 Fakes框架的TDD实战——接口模拟

基于VS2012 Fakes框架的TDD实战——接口模拟

〇、目录

一、 前言

二、 需求说明

三、 项目结构

四、 开发准备

  (一)  应用代码准备

  (二)  测试类准备

  (三)  TDD正式开始

五、 总结

六、 源码下载

七、 参考资料

一、前言

  最近团队要尝试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~ 努力!

六、源码下载

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

Expression 序列化

 

【Expression 序列化】WCF的简单使用及其Expression Lambada的序列化问题初步解决方案(四)——关于Guid的问题

摘要: 发了本系列的前三遍几天后,收到了若风云同学的站内信,说如果Expression中包含Guid类型属性的查询时,会报异常,亲自验证了下,确实会有问题。原因是Dynamic Expression API 与 ExpressionSerialization 对Guid的支持不是很好。下面就来解决这个问题。首先,给我们的DataContract(Member类)增加一个Guid类型的属性UserCode,同时Service的DataSource也作相应的修改:WCF的DataContract: 1 [DataContract] 2 public class Member 3 { 4 [Data... 阅读全文

posted @  2012-04-22 08:11  郭明锋 阅读(1054) |  评论 (9)   编辑

 

【Expression 序列化】WCF的简单使用及其Expression Lambada的序列化问题初步解决方案(三)

摘要: 接上文【Expression 序列化】WCF的简单使用及其Expression Lambada的序列化问题初步解决方案(二) 上文最后留下了一个问题,引起这个问题的操作是把原来通过硬编码字符串来设置的Expression参数改为接收用户输入。这是个非常正常的需求,可以说如果这个问题不解决,上文的Expression序列化的方法是无法应用到实际项目中的。下面来分析异常引起的原因。 首先,来查看一下接收输入来组装的Expression与硬编码的方式生成有什么不同: 1 private static void Method02() 2 { 3 Expression<Func<Memb.. 阅读全文

posted @  2012-04-11 01:53  郭明锋 阅读(466) |  评论 (15)   编辑

 

【Expression 序列化】WCF的简单使用及其Expression Lambada的序列化问题初步解决方案(二)

摘要: 接上文【Expression 序列化】WCF的简单使用及其Expression Lambada的序列化问题初步解决方案(一)上文留下了一个问题没有处理,但最后也找到了相应的解决方案,下面就来说下问题的解决Expression Tree Serializer提供的解决方案是 把Expression表达式树转换为XElement类型的XML数据,传输到服务端,再反转换还原成原来的Expression表达式所以,客户端与服务端之间传送的数据是XElement类型的数据了,从而避开了Expression类型不能序列化的问题我们先来了解一下Expression Tree Serializer的使用,下载 阅读全文

posted @  2012-04-10 03:10  郭明锋 阅读(553) |  评论 (3)   编辑

 

【Expression 序列化】WCF的简单使用及其Expression Lambada的序列化问题初步解决方案(一)

摘要: 在园子里混迹多年,始终保持着“只看帖不回帖”的习惯,看了很多,学了很多,却从不敢写些东西贴出来,一来没什么可写的,二来水平不够,怕误人子弟……最近在做一个MVC+WCF+EF的项目,遇到问题不少,但大多数问题都是前人遇到并解决了的,感谢园子里的大牛们的无私奉献。俗话说“礼尚往来”,我也在此分享一个最近在项目中遇到的问题,就是远程调用时的Expression表达式的序列化问题的初始解决方案,希望抛出的这块石头能引出完美的钻石来,同时第一次写博客,请大家多多赐教……为了说明问题,我将用一个简单的示例来演示,文章的最后会有示例的源代码下载。示例说明:演示项目还是使用传统的四层结构:WCF服务契约:契 阅读全文

posted @  2012-04-10 00:30  郭明锋 阅读(655) |  评论 (6)   编辑

当前标签: 架构设计

 

MVC实用构架实战(二)——优雅的URL

 

MVC实用构架实战(一)——使用MEF实现IOC

郭明锋 2013-03-14 14:20 阅读:839 评论:4

当前标签: Fakes框架

 

基于VS2012 Fakes框架的TDD实战——私有成员,静态成员模拟

 

基于VS2012 Fakes框架的TDD实战——接口模拟

 

 

当前标签: JQuery

 

jQuery 常见操作实现方式

 

 

当前标签: 正则表达式

 

常用正则表达式收藏

 

 

 

 

分类:  TDD

标签:  TDD实战 ,  Fakes框架

作者: Leo_wl

    

出处: http://www.cnblogs.com/Leo_wl/

    

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

版权信息

查看更多关于基于VS2012 Fakes框架的TDD实战——接口模拟的详细内容...

  阅读:44次