找回密码
 立即注册
首页 业界区 业界 基于VS2012 Fakes框架的TDD实战——接口模拟 ...

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

庞悦 2025-5-29 16:17:21
前言

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

需求说明

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

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

项目结构

1.png

  先分解一下项目的结构,还是传统的三层结构,从底层到上层:

  • 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. 1     /// <summary>
  2. 2     ///   数据实体类基类,定义数据库存储的数据结构的通用部分
  3. 3     /// </summary>
  4. 4     public abstract class Entity
  5. 5     {
  6. 6         /// <summary>
  7. 7         ///   编号
  8. 8         /// </summary>
  9. 9         public int Id { get; set; }
  10. 10
  11. 11         /// <summary>
  12. 12         ///   是否逻辑删除(相当于回收站,非物理删除)
  13. 13         /// </summary>
  14. 14         public bool IsDelete { get; set; }
  15. 15
  16. 16         /// <summary>
  17. 17         ///   添加时间
  18. 18         /// </summary>
  19. 19         public DateTime AddDate { get; set; }
  20. 20     }
复制代码
IRepository:通用数据访问接口,简单起见,只写了几个增删改查的接口
  1. 1     /// <summary>
  2. 2     /// 定义仓储模式中的数据标准操作,其实现类是仓储类型。
  3. 3     /// </summary>
  4. 4     /// <typeparam name="TEntity">要实现仓储的类型</typeparam>
  5. 5     public interface IRepository<TEntity> where TEntity : Entity
  6. 6     {
  7. 7         #region 公用方法
  8. 8
  9. 9         /// <summary>
  10. 10         ///   插入实体记录
  11. 11         /// </summary>
  12. 12         /// <param name="entity"> 实体对象 </param>
  13. 13         /// <param name="isSave"> 是否执行保存 </param>
  14. 14         /// <returns> 操作影响的行数 </returns>
  15. 15         int Insert(TEntity entity, bool isSave = true);
  16. 16
  17. 17         /// <summary>
  18. 18         ///   删除实体记录
  19. 19         /// </summary>
  20. 20         /// <param name="entity"> 实体对象 </param>
  21. 21         /// <param name="isSave"> 是否执行保存 </param>
  22. 22         /// <returns> 操作影响的行数 </returns>
  23. 23         int Delete(TEntity entity, bool isSave = true);
  24. 24
  25. 25         /// <summary>
  26. 26         ///   更新实体记录
  27. 27         /// </summary>
  28. 28         /// <param name="entity"> 实体对象 </param>
  29. 29         /// <param name="isSave"> 是否执行保存 </param>
  30. 30         /// <returns> 操作影响的行数 </returns>
  31. 31         int Update(TEntity entity, bool isSave = true);
  32. 32
  33. 33         /// <summary>
  34. 34         /// 提交当前的Unit Of Work事务,作用与 IUnitOfWork.Commit() 相同。
  35. 35         /// </summary>
  36. 36         /// <returns>提交事务影响的行数</returns>
  37. 37         int Commit();
  38. 38
  39. 39         /// <summary>
  40. 40         ///   查找指定编号的实体记录
  41. 41         /// </summary>
  42. 42         /// <param name="id"> 指定编号 </param>
  43. 43         /// <returns> 符合编号的记录,不存在返回null </returns>
  44. 44         TEntity GetById(object id);
  45. 45
  46. 46         /// <summary>
  47. 47         /// 查找指定名称的实体记录,注意:如实体无名称属性则不支持
  48. 48         /// </summary>
  49. 49         /// <param name="name">名称</param>
  50. 50         /// <returns>符合名称的记录,不存在则返回null</returns>
  51. 51         /// <exception cref="NotSupportedException">当对应实体无名称时引发将引发异常</exception>
  52. 52         TEntity GetByName(string name);
  53. 53
  54. 54         #endregion
  55. 55     }
复制代码
Member:实体类——用户信息
  1. 1     /// <summary>
  2. 2     ///   实体类——用户信息
  3. 3     /// </summary>
  4. 4     public class Member : Entity
  5. 5     {
  6. 6         public string UserName { get; set; }
  7. 7
  8. 8         public string Password { get; set; }
  9. 9
  10. 10         public string Email { get; set; }
  11. 11     }
复制代码
MemberInactive:实体类——未激活用户信息
  1. 1     /// <summary>
  2. 2     ///   实体类——未激活用户信息
  3. 3     /// </summary>
  4. 4     public class MemberInactive : Entity
  5. 5     {
  6. 6         public string UserName { get; set; }
  7. 7
  8. 8         public string Password { get; set; }
  9. 9
  10. 10         public string Email { get; set; }
  11. 11     }
复制代码
ConfigInfo:实体类——系统配置信息
  1. 1     /// <summary>
  2. 2     ///   实体类——系统配置信息
  3. 3     /// </summary>
  4. 4     public class ConfigInfo : Entity
  5. 5     {
  6. 6         public ConfigInfo()
  7. 7         {
  8. 8             RegisterConfig = new RegisterConfig();
  9. 9         }
  10. 10
  11. 11         public RegisterConfig RegisterConfig { get; set; }
  12. 12     }
  13. 13
  14. 14
  15. 15     public class RegisterConfig
  16. 16     {
  17. 17         /// <summary>
  18. 18         ///   注册时是否需要Email激活
  19. 19         /// </summary>
  20. 20         public bool NeedActive { get; set; }
  21. 21
  22. 22         /// <summary>
  23. 23         ///   激活邮件有效期,单位:分钟
  24. 24         /// </summary>
  25. 25         public int ActiveTimeout { get; set; }
  26. 26
  27. 27         /// <summary>
  28. 28         ///   允许同一Email注册不同会员
  29. 29         /// </summary>
  30. 30         public bool EmailRepeat { get; set; }
  31. 31     }
复制代码
IMemberDao:数据访问接口——用户信息,仅添加IRepository不满足的接口
  1. 1     /// <summary>
  2. 2     ///   数据访问接口——用户信息
  3. 3     /// </summary>
  4. 4     public interface IMemberDao : IRepository<Member>
  5. 5     {
  6. 6         /// <summary>
  7. 7         ///   由电子邮箱查找用户信息
  8. 8         /// </summary>
  9. 9         /// <param name="email"> 电子邮箱地址 </param>
  10. 10         /// <returns> </returns>
  11. 11         IEnumerable<Member> GetByEmail(string email);
  12. 12     }
复制代码
IMemberInactiveDao:数据访问接口——未激活用户信息,仅添加IRepository不满足的接口
  1. 1     /// <summary>
  2. 2     ///   数据访问接口——未激活用户信息
  3. 3     /// </summary>
  4. 4     public interface IMemberInactiveDao : IRepository<MemberInactive>
  5. 5     {
  6. 6         /// <summary>
  7. 7         ///   由电子邮箱获取未激活的用户信息
  8. 8         /// </summary>
  9. 9         /// <param name="email"> 电子邮箱地址 </param>
  10. 10         /// <returns> </returns>
  11. 11         IEnumerable<MemberInactive> GetByEmail(string email);
  12. 12     }
复制代码
IConfigInfoDao:数据访问接口——系统配置,无额外需求的接口,所以为空接口
  1. 1     /// <summary>
  2. 2     ///   数据访问接口——系统配置信息
  3. 3     /// </summary>
  4. 4     public interface IConfigInfoDao : IRepository<ConfigInfo>
  5. 5     { }
复制代码
IAccountContract:账户模块业务契约——定义了三个操作,用作注册前的数据检查和注册提交
  1. 1     /// <summary>
  2. 2     ///   核心业务契约——账户模块
  3. 3     /// </summary>
  4. 4     public interface IAccountContract
  5. 5     {
  6. 6         /// <summary>
  7. 7         /// 用户名重复检查
  8. 8         /// </summary>
  9. 9         /// <param name="userName">用户名</param>
  10. 10         /// <param name="configName">系统配置名称</param>
  11. 11         /// <returns></returns>
  12. 12         bool UserNameExistsCheck(string userName, string configName);
  13. 13
  14. 14         /// <summary>
  15. 15         /// 电子邮箱重复检查
  16. 16         /// </summary>
  17. 17         /// <param name="email">电子邮箱</param>
  18. 18         /// <param name="configName">系统配置名称</param>
  19. 19         /// <returns></returns>
  20. 20         bool EmailExistsCheck(string email, string configName);
  21. 21         
  22. 22         /// <summary>
  23. 23         /// 用户注册
  24. 24         /// </summary>
  25. 25         /// <param name="model">注册信息模型</param>
  26. 26         /// <param name="configName">系统配置名称</param>
  27. 27         /// <returns></returns>
  28. 28         RegisterResults Register(Member model, string configName);
  29. 29     }
复制代码
以上代码本来想收起来的,但测试时代码展开老失效,所以辛苦大家划了那麽长的鼠标来看下面的正题了\(^o^)/

测试类准备


  • 添加测试项目的引用
    2.png

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

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

  • 打开对象查看器,可看到生成的Fakes程序集的内容,所有的接口都生成了一个对应的模拟类 
    5.png

  • 通过ILSpy对Fakes程序集进行反向,可以看到生成的模拟类如下所示,StubIMemberDao实现了接口IMemberDao,而接口中的公共成员都生成了“方法名+参数类型名”的委托模拟,用以接收外部给模拟方法的执行结果赋值,这样每个方法的返回值都可以被控制
    6.png

  • 另外生成的Fakes文件夹中的配置文件Liuliu.Demo.Core.fakes内容如下所示
    1. 1 <Fakes xmlns="http://schemas.microsoft.com/fakes/2011/">
    2. 2   
    3. 3 </Fakes>
    复制代码
     这个配置默认会把测试程序集中的所有接口、类都生成模拟类,当然也可以配置生成指定的类型的模拟,相关知识这里就不讲了,请参阅官方文档:Microsoft Fakes 中的代码生成、编译和命名约定
  • 需要特别说明的是,每次生成,Fakes程序集都会重新生成,所以测试类有更改后想刷新Fakes程序集,只需要把原来的程序集删除再进行生成,或者在测试项目能编译的时候重新编译测试项目即可。

TDD正式开始


总结

  看起来文章写得挺长了,其实内容并没有多少,篇幅都被代码拉开了。我们来总结一下使用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
 
 
 
 
 
 
 
作者:郭明锋
Q群:MVC EF技术交流(5008599)
7.png
OSharp开发框架交流(85895249)
8.png

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

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册