• .NET 测试驱动开发(TDD)之封装数据库以便Mock测试


    在测试驱动开发中,对数据库特别是ORM的测试,有的时候不好做,这里介绍我们的做法。

    本文的方案是基于Entity Framework 4.0 Code First, Autofac的。

    Entity Framework 4.0 Code First对测试驱动的支持

    由于Entity Framework 4.0 Code First可以从业务层的简单C#对象(POCO)反向生成数据库以及数据库相应的表,如果数据简单的话,那么就直接实行TDD模式:

    1、 首先创建测试用例,这里我们以一个客户关系管理系统为例讲解,用例是测试保存客户资料的功能:

       1:      [TestMethod]
       2:      public void 测试保存客户资料功能()
       3:      {
       4:          using (var rep = new CrmContext())
       5:          {
       6:              var customer = Customer.New<Customer>();
       7:              customer.Name = "上海知平";
       8:              rep.Customers.Add(customer);
       9:   
      10:              rep.SaveChanges();
      11:          }
      12:      }

    2、 补充几个必要的类型:

       1:   
       2:  public class Customer : ISecret, INamedTable
       3:  {
       4:      public Guid Id { get; set; }
       5:   
       6:      public int Permission
       7:      {
       8:          get;
       9:          set;
      10:      }
      11:   
      12:      public string Name
      13:      {
      14:          get;
      15:          set;
      16:      }
      17:   
      18:      public static T New<T>() where T : Customer, new()
      19:      {
      20:          var ret = new T()
      21:          {
      22:              Id = Guid.NewGuid()
      23:          };
      24:   
      25:          return ret;
      26:      }
      27:  }
      28:   
      29:  public class CrmContext : DbContext
      30:  {
      31:      public CrmContext() { }
      32:   
      33:      public CrmContext(string nameOrConnectionString)
      34:   
      35:          : base(nameOrConnectionString)
      36:      {
      37:      }
      38:   
      39:      public DbSet<Customer> Customers { get; set; }
      40:  }

    其中Customer是OR映射过的类型,用以在数据库和业务层之间加载和保存客户资料;而CrmContext就可以理解成数据库,里面有一个表Customers,当然这个是经过Entity Framework做OR映射后的结果。

    3、 在测试工程的app.config文件里,添加CrmContext的链接字符串:

       1:  <?xml version="1.0" encoding="utf-8"?>
       2:  <configuration>
       3:    <connectionStrings>
       4:      <add name="Vowei.Data.VoweiContextImpl" connectionString="Data Source=.\SQLEXPRESS;Integrated Security=SSPI;Database=TaskConnect1" providerName="System.Data.SqlClient" />
       5:    </connectionStrings>
       6:  </configuration>

    4、 这个时候运行测试用例就可以看到数据库已经自动生成了,而且也可以看到数据已经插入。

    在EF的基础上再封装一层

    从上文中可以看到,EF对测试驱动的支持已经很好了,但为什么还需要再封装一层呢?主要是出于两个目的:

    1、 有些数据表,比如跟别的数据有比较复杂的联系,代码在设计时,一时半会不好确定对数据的建模是否合理,API设计是否流畅,因此为了保险起见,先在内存里模拟一个数据库,确定API设计合理之后,再将数据之间的关系通过EF映射到数据库上。

    2、 将业务层和数据层的细节分离开来,比如业务层可能在后续版本使用RESTful API获取数据。

    下面是我们再封装的过程。

    1、 首先将CrmContext的属性和方法提成一个接口:

       1:  public interface IContext : IDisposable
       2:  {
       3:      IRepository<Customer> Customers { get; }
       4:   
       5:      int SaveChanges();
       6:  }

    因为需要完全和EF分离出来,需要把CrmContext里的DbSet<Customer>类型的Customers属性也分离出一个接口。

       1:  /// <summary>
       2:  /// 代表数据库中的一个表
       3:  /// </summary>
       4:  /// <typeparam name="T">OR映射里的类型</typeparam>
       5:  public interface IRepository<T> where T : class
       6:  {
       7:      /// <summary>
       8:      /// 获取数据表的名称(在数据库中对应的表) 
       9:      /// </summary>
      10:      string Name { get; }
      11:   
      12:      /// <summary>
      13:      /// 返回一个组合后的IQueryable查询
      14:      /// </summary>
      15:      IQueryable<T> Query { get; }
      16:   
      17:      /// <summary>
      18:      /// 添加外键查询
      19:      /// </summary>
      20:      /// <param name="navigationProperty">OR映射中对象的外键属性</param>
      21:      /// <returns>返回对象本身,以达到IRepository.Include("Property1").Include("Property2").Query的效果</returns>
      22:      IRepository<T> Include(string navigationProperty);
      23:   
      24:      /// <summary>
      25:      /// 往数据层中添加一个新的对象
      26:      /// </summary>
      27:      /// <param name="item">新对象</param>
      28:      void Add(T item);
      29:   
      30:      /// <summary>
      31:      /// 在数据层中删除一个对象
      32:      /// </summary>
      33:      /// <param name="item">要删除的对象</param>
      34:      void Remove(T item);
      35:   
      36:      /// <summary>
      37:      /// 用于更新操作时,将尚未和数据库关联的对象关联
      38:      /// </summary>
      39:      /// <param name="entity">尚未和数据库关联的对象</param>
      40:      /// <returns>一个已经和数据库关联的对象</returns>
      41:      T Attach(T entity);
      42:   
      43:      IQueryable<T> SqlQuery(string sql, params object[] parameters);
      44:   
      45:      /// <summary>
      46:      /// 获取所属的数据库
      47:      /// </summary>
      48:      IContext Context { get; }
      49:  }

    2、 实现接口IContext,并且将具体的实现隐藏。

       1:   
       2:  public class CrmContext : IContext
       3:  {
       4:      private CrmContextImpl _contextImpl;
       5:   
       6:      internal CrmContextImpl Impl { get { return _contextImpl; } }
       7:   
       8:      static CrmContext()
       9:      {
      10:          Database.SetInitializer(new CrmContextInitializer());
      11:      }
      12:   
      13:      public CrmContext() :
      14:          this(new CrmContextImpl())
      15:      {
      16:      }
      17:   
      18:      public CrmContext(string nameOrConnectionString)
      19:          : this(new CrmContextImpl(nameOrConnectionString))
      20:      {
      21:      }
      22:   
      23:      public IRepository<Customer> Customers
      24:      {
      25:          get;
      26:          private set;
      27:      }
      28:   
      29:      public int SaveChanges()
      30:      {
      31:          return _contextImpl.SaveChanges();
      32:      }
      33:   
      34:      public void Dispose()
      35:      {
      36:          if (_contextImpl != null)
      37:          {
      38:              _contextImpl.Dispose();
      39:              _contextImpl = null;
      40:          }
      41:      }
      42:  }

    3、 为了避免对每个数据表都重复实现IRepository这个接口,做了一个通用的接口实现类型,通过反射将IContext的数据表属性和具体实现的数据表属性关联起来。

       1:   
       2:  internal class RepositoryImpl<T, U> : IRepository<U>
       3:      where T : class
       4:      where U : class, T
       5:  {
       6:      // 被封装的数据表实现方式
       7:      private DbSet<U> _table;
       8:      // 保存上一次类似Where等Lambda调用保存的表达式树
       9:      private IQueryable<U> _query;
      10:      // 保存新数据的回调函数
      11:      private Func<T, U> _persistRouing;
      12:      private string _tableName;
      13:      private VoweiContext _context;
      14:   
      15:      // 这个变量仅仅是用来在实现继承类型,避免编译器混乱用的
      16:      public IRepository<T> IfImplementation { get; private set; }
      17:   
      18:      private RepositoryImpl(IQueryable<U> query, Func<T, U> persistRouing, string tableName)
      19:      {
      20:          _query = query;
      21:          _persistRouing = persistRouing;
      22:          _tableName = tableName;
      23:      }
      24:   
      25:      public RepositoryImpl(VoweiContext context, Func<T, U> persistRouing, string tableName)
      26:      {
      27:          var property = typeof(VoweiContextImpl).GetProperty(tableName);
      28:          // 通过反射的机制,根据“tableName”参数给出的属性名,获取实现IContext某个数据表的具体对象引用
      29:          // 并保存到类型变量里,以便将所有的查询、Include、增删改等操作传递给这个对象。
      30:          _table = (DbSet<U>)property.GetValue(context._contextImpl, new object[] { });
      31:          _query = _table;
      32:          _persistRouing = persistRouing;
      33:          _tableName = tableName;
      34:          _context = context;
      35:   
      36:          // 如果类型T和U不是同一个类型,说明要么U继承与T,或者U实现了T这个接口
      37:          // 这样一来,为了规避编译器编译错误,需要再封一层
      38:          if (typeof(T) != typeof(U))
      39:              IfImplementation = new RepositoryIfImpl(this, tableName);
      40:          else // 否则就很简单了
      41:              IfImplementation = (IRepository<T>)this;
      42:      }
      43:   
      44:      public RepositoryImpl(VoweiContext context)
      45:          : this(context, null, typeof(U).Name)
      46:      {
      47:      }
      48:   
      49:      /// <summary>
      50:      /// 获取该Repository对应的数据库里的表名
      51:      /// </summary>
      52:      public string Name { get { return _tableName; } }
      53:   
      54:      public IContext Context { get { return _context; } }
      55:   
      56:      public IQueryable<U> Query
      57:      {
      58:          get
      59:          {
      60:              if (_query == null)
      61:                  return _table;
      62:              else
      63:                  return _query;
      64:          }
      65:      }
      66:   
      67:      public IRepository<U> Include(string navigationProperty)
      68:      {
      69:          if (_query == null)
      70:              return new RepositoryImpl<T, U>(_table.Include(navigationProperty), _persistRouing, _tableName);
      71:          else
      72:              return new RepositoryImpl<T, U>(_query.Include(navigationProperty), _persistRouing, _tableName) { _table = _table };
      73:      }
      74:   
      75:      public IQueryable<U> SqlQuery(string sql, params object[] parameters)
      76:      {
      77:          return _table.SqlQuery(sql, parameters).AsQueryable<U>();
      78:      }
      79:   
      80:      public virtual void Add(U item)
      81:      {
      82:          _table.Add(item);
      83:      }
      84:   
      85:      public virtual void Remove(U item)
      86:      {
      87:          _table.Remove(item);
      88:      }
      89:   
      90:      public U Attach(U entity)
      91:      {
      92:          return _table.Attach(entity);
      93:      }
      94:   
      95:      class RepositoryIfImpl : IRepository<T>
      96:      {
      97:          private RepositoryImpl<T, U> _outer;
      98:          private IQueryable<U> _tmpQuery;
      99:          private string _tableName;
     100:   
     101:          public RepositoryIfImpl(RepositoryImpl<T, U> outer, string tableName)
     102:          {
     103:              _outer = outer;
     104:              _tableName = tableName;
     105:          }
     106:   
     107:          public string Name { get { return _tableName; } }
     108:   
     109:          public IContext Context { get { return _outer._context; } }
     110:   
     111:          public IQueryable<T> Query
     112:          {
     113:              get
     114:              {
     115:                  if (_tmpQuery == null)
     116:                      return _outer._table;
     117:                  else
     118:                      return _tmpQuery;
     119:              }
     120:          }
     121:   
     122:          public IQueryable<T> SqlQuery(string sql, params object[] parameters)
     123:          {
     124:              return _outer._table.SqlQuery(sql, parameters).AsQueryable<U>();
     125:          }
     126:   
     127:          public IRepository<T> Include(string navigationProperty)
     128:          {
     129:              var result = new RepositoryIfImpl(_outer, _tableName);
     130:              result._tmpQuery = _tmpQuery == null ? _outer._table.Include(navigationProperty)
     131:                                                   : _tmpQuery.Include(navigationProperty);
     132:   
     133:              return result;
     134:          }
     135:   
     136:          public virtual void Add(T item)
     137:          {
     138:              if (_outer._persistRouing == null)
     139:                  throw new InvalidOperationException("当需要从基类T的对象实例生成一个派生类型U的实例时,需要指明转换的方式!");
     140:              _outer._table.Add(_outer._persistRouing(item));
     141:          }
     142:   
     143:          public virtual void Remove(T item)
     144:          {
     145:              _outer._table.Remove((U)item);
     146:          }
     147:   
     148:          public T Attach(T entity)
     149:          {
     150:              return _outer.Attach((U)entity);
     151:          }
     152:      }
     153:  }

    从上面的代码里可以看到,RepositoryImpl和RepositoryIfImpl两个类是内部类,而且是CrmContext的内部类,避免了被系统其他代码调用到的机会。

    4、 因为IRepository这个接口是一个通用接口,所以需要一个机制映射IContext的数据表属性和CrmContextImpl的数据表属性,下面两个函数就是用来做映射的。

       1:      // 如果封装的类型和数据库里的其他类型没有继承关系,则使用这个函数执行映射
       2:      private IRepository<T> RegisterTable<T>(string tableName)
       3:      where T : class
       4:      {
       5:          var result = new RepositoryImpl<T, T>(this, null, tableName);
       6:          _tableMap.Add(typeof(T), result);
       7:          return result;
       8:      }
       9:   
      10:      // 如果封装的类型和数据库里的其他类型有继承关系,则使用这个函数执行映射,
      11:      // 需要指定类型和类型的基类
      12:      private IRepository<T> RegisterDeliveredTable<T, U>(Func<T, U> persistRouting, string tableName)
      13:          where T : class
      14:          where U : class, T
      15:      {
      16:          var result = new RepositoryImpl<T, U>(this, persistRouting, tableName);
      17:          _tableMap.Add(typeof(T), result.IfImplementation);
      18:          _tableMap.Add(typeof(U), result);
      19:          return result.IfImplementation;
      20:      }

    5、 通用的接口实现封装好了以后,加一个新的数据表就是一个注册的过程,这个过程在构造函数里就做了。

       1:      internal VoweiContext(VoweiContextImpl impl)
       2:      {
       3:          _contextImpl = impl;
       4:          Customers = RegisterTable<Customer>("Customers");
       5:      }

    6、 再在测试用例或者程序启动合适的地方,通过Ioc机制将数据库接口IContext和实现接口的对象注册一番就可以用了。

       1:  var builder = new ContainerBuilder();
       2:  builder.RegisterType<CrmContext>().AsImplementedInterfaces();
       3:  IocHelper.Container = builder.Build();

    7、 最后用的时候很简单,本文开头的例子就改成使用Ioc的方式从容器里获取一个接口实现:

       1:      [TestMethod]
       2:      public void 测试保存客户资料功能()
       3:      {
       4:          using (var rep = IocHelper.Container.Resolve<IContext>())
       5:          {
       6:              var customer = Customer.New<Customer>();
       7:              customer.Name = "上海知平";
       8:              rep.Customers.Add(customer);
       9:              rep.SaveChanges();
      10:          }
      11:      }

    本文由知平软件 施懿民编写,请关注我们的微博

  • 相关阅读:
    384. 最长无重复字符的子串
    406. 和大于S的最小子数组
    159. 寻找旋转排序数组中的最小值
    62. 搜索旋转排序数组
    20. 骰子求和
    125. 背包问题 II
    92. 背包问题
    1295. 质因数统计
    471. 最高频的K个单词
    1339. 最大区间
  • 原文地址:https://www.cnblogs.com/vowei/p/2685793.html
Copyright © 2020-2023  润新知