Visual Studio 2012 Fakes框架测试驱动开发TDD教程

 

一、前言

  最近团队要尝试TDD(测试驱动开发)的实践,很多人习惯了先代码后测试的流程,对于TDD总心存恐惧,认为没有代码的情况下写测试代码时被架空了,没法写下来,其实,根据个人实践经验,TDD并不可怕,还很可爱,只要你真正去实践了几十个测试用例之后,你会爱上这种形式方式的。微软对于TDD的开发方式是大力支持和推荐的,新发布的VS2012的团队模板就是根据。新的Visual Studio 2012给我们带来了Fakes框架,这是一个针对代码测试时对测试的外界依赖(如数据库,文件等)进行模拟的Mock框架,用上了之后,我立即从Moq的阵营中叛变了^_^。截止到写此文的时间,网上还没有一篇关于Fakes框架的文章(除了“VS11将拥有更好的单元测试工具和Fakes框架”这篇介绍性的之外),就让我们来慢慢摸索着用吧。废话少说,下面我们就来一步一步的使用Visual Studio 2012的Fakes框架来实战一把TDD。

二、需求说明

  我们要做的是一个普通的用户注册中“检查用户名是否存在”的功能,需求如下:

  1. 用户名不能重复
  2. 可设置是否启用邮件激活,如果不启用邮件激活,则直接在“正式用户信息表”中检查,反之则还要进入“未激活用户信息表”中进行查询

三、项目结构

【Visual Studio 2012 Fakes框架测试驱动开发TDD教程】1、接口模拟_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项目

  其他的项目与测试无关,略过。

四、开发准备

(一) 应用代码准备

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:实体类——系统配置信息

    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教程】1、接口模拟(4)_www.fengfly.com

  2. 添加要模拟实现接口的Fakes程序集,要模拟的接口在Liuliu.Demo.Core程序集中,所以在该程序集上点右键,选择“添加Fakes程序集”菜单项

    【Visual Studio 2012 Fakes框架测试驱动开发TDD教程】1、接口模拟(4)_www.fengfly.com

  3. 添加好了之后,Fakes框架会在测试项目中添加一个Fakes文件夹和一个配置文件,并自动生成引用一个 模拟程序集.Fakes 的程序集和Fakes框架的运行环境Microsoft.QualityTools.Testing.Fakes

    【Visual Studio 2012 Fakes框架测试驱动开发TDD教程】1、接口模拟(4)_www.fengfly.com

  4. 打开对象查看器,可看到生成的Fakes程序集的内容,所有的接口都生成了一个对应的模拟类
    【Visual Studio 2012 Fakes框架测试驱动开发TDD教程】1、接口模拟(4)_www.fengfly.com
  5. 通过ILSpy对Fakes程序集进行反向,可以看到生成的模拟类如下所示,StubIMemberDao实现了接口IMemberDao,而接口中的公共成员都生成了“方法名+参数类型名”的委托模拟,用以接收外部给模拟方法的执行结果赋值,这样每个方法的返回值都可以被控制【Visual Studio 2012 Fakes框架测试驱动开发TDD教程】1、接口模拟(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教程】1、接口模拟(5)_www.fengfly.com
参数的字符串,值的有效性一定要检查的,所以添加以下两个测试用例,通过ExpectedException特性可能确定抛出异常的类型

    1. 运行测试,结果如下,原因为还没有写异常代码,期望的异常没有引发。└(^o^)┘平常我们很怕出异常,现在要去期望出异常

      【Visual Studio 2012 Fakes框架测试驱动开发TDD教程】1、接口模拟(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教程】1、接口模拟(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教程】1、接口模拟(8)_www.fengfly.com

五、总结

  看起来文章写得挺长了,其实内容并没有多少,篇幅都被代码拉开了。我们来总结一下使用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

 

 

posted @ 2013-10-15 12:33  手撕.net  阅读(619)  评论(1编辑  收藏  举报