【问题标题】:Unit Testing Dapper with Inline Queries使用内联查询对 Dapper 进行单元测试
【发布时间】:2015-05-26 08:52:11
【问题描述】:

我知道有几个问题与我的相似。

但我认为上述两个问题都没有符合我要求的明确答案。

现在我开发了一个新的 WebAPI 项目,并在 WebAPI 项目和 DataAccess 技术之间进行了拆分。因为我可以模拟数据访问类,所以我测试 WebAPI 的控制器没有问题。

但是对于 DataAccess 类,这是一个不同的故事,因为我使用的是带有内联查询的 Dapper,所以我有点困惑如何使用单元测试来测试它。我问了一些朋友,他们更喜欢做集成测试而不是单元测试。

我想知道的是,是否可以对其中使用 Dapper 和 Inline 查询的 DataAccess 类进行单元测试。

假设我有一个这样的类(这是一个通用的存储库类,因为很多代码都有类似的查询,通过表名和字段来区分)

public abstract class Repository<T> : SyncTwoWayXI, IRepository<T> where T : IDatabaseTable
{
       public virtual IResult<T> GetItem(String accountName, long id)
       {
            if (id <= 0) return null;

            SqlBuilder builder = new SqlBuilder();
            var query = builder.AddTemplate("SELECT /**select**/ /**from**/ /**where**/");

            builder.Select(string.Join(",", typeof(T).GetProperties().Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof(SqlMapperExtensions.DapperIgnore))).Select(p => p.Name)));
            builder.From(typeof(T).Name);
            builder.Where("id = @id", new { id });
            builder.Where("accountID = @accountID", new { accountID = accountName });
            builder.Where("state != 'DELETED'");

            var result = new Result<T>();
            var queryResult = sqlConn.Query<T>(query.RawSql, query.Parameters);

            if (queryResult == null || !queryResult.Any())
            {
                result.Message = "No Data Found";
                return result;
            }

            result = new Result<T>(queryResult.ElementAt(0));
            return result;
       }

       // Code for Create, Update and Delete
  }

上面代码的实现是这样的

public class ProductIndex: IDatabaseTable
{
        [SqlMapperExtensions.DapperKey]
        public Int64 id { get; set; }

        public string accountID { get; set; }
        public string userID { get; set; }
        public string deviceID { get; set; }
        public string deviceName { get; set; }
        public Int64 transactionID { get; set; }
        public string state { get; set; }
        public DateTime lastUpdated { get; set; }
        public string code { get; set; }
        public string description { get; set; }
        public float rate { get; set; }
        public string taxable { get; set; }
        public float cost { get; set; }
        public string category { get; set; }
        public int? type { get; set; }
}

public class ProductsRepository : Repository<ProductIndex>
{
   // ..override Create, Update, Delete method
}

【问题讨论】:

    标签: c# unit-testing dapper


    【解决方案1】:

    这是我们的方法:

    1. 首先,您需要在 IDbConnection 之上有一个抽象,以便能够模拟它:

      public interface IDatabaseConnectionFactory
      {
          IDbConnection GetConnection();
      }
      
    2. 您的存储库将从该工厂获取连接并对其执行Dapper 查询:

      public class ProductRepository
      {
          private readonly IDatabaseConnectionFactory connectionFactory;
      
          public ProductRepository(IDatabaseConnectionFactory connectionFactory)
          {
              this.connectionFactory = connectionFactory;
          }
      
          public Task<IEnumerable<Product>> GetAll()
          {
              return this.connectionFactory.GetConnection().QueryAsync<Product>(
                  "select * from Product");
          }
      }
      
    3. 您的测试将创建一个包含一些示例行的内存数据库,并检查存储库如何检索它们:

      [Test]
      public async Task QueryTest()
      {
          // Arrange
          var products = new List<Product>
          {
              new Product { ... },
              new Product { ... }
          };
          var db = new InMemoryDatabase();
          db.Insert(products);
          connectionFactoryMock.Setup(c => c.GetConnection()).Returns(db.OpenConnection());
      
          // Act
          var result = await new ProductRepository(connectionFactoryMock.Object).GetAll();
      
          // Assert
          result.ShouldBeEquivalentTo(products);
      }
      
    4. 我想有多种方法可以实现这样的内存数据库;我们在SQLite 数据库之上使用了OrmLite

      public class InMemoryDatabase
      {
          private readonly OrmLiteConnectionFactory dbFactory = new OrmLiteConnectionFactory(":memory:", SqliteOrmLiteDialectProvider.Instance);
      
          public IDbConnection OpenConnection() => this.dbFactory.OpenDbConnection();
      
          public void Insert<T>(IEnumerable<T> items)
          {
              using (var db = this.OpenConnection())
              {
                  db.CreateTableIfNotExists<T>();
                  foreach (var item in items)
                  {
                      db.Insert(item);
                  }
              }
          }
      }
      

    【讨论】:

    • 只有一件事,工厂不需要获取IDbConnection 的抽象(它已经是一个接口),而是能够在repo 中建立新的连接。如果您不需要它(并且可能您不需要在 Web API 请求的上下文中创建多个连接),您可以直接将 IDbConnection 传递给 repo。
    • @IgnacioCalvo 我们同时需要多个连接,例如在进行可以并行运行的异步查询时。
    • connectionFactoryMock 我在哪里可以找到这个实例?
    • 我们使用了Moq库,所以var connectionFactoryMock = new Mock&lt;IDatabaseConnectionFactory&gt;();
    【解决方案2】:

    我调整了 @Mikhail 所做的,因为我在添加 OrmLite 包时遇到了问题。

    internal class InMemoryDatabase
    {
        private readonly IDbConnection _connection;
    
        public InMemoryDatabase()
        {
            _connection = new SQLiteConnection("Data Source=:memory:");
        }
    
        public IDbConnection OpenConnection()
        {
            if (_connection.State != ConnectionState.Open)
                _connection.Open();
            return _connection;
        }
    
        public void Insert<T>(string tableName, IEnumerable<T> items)
        {
            var con = OpenConnection();
    
            con.CreateTableIfNotExists<T>(tableName);
            con.InsertAll(tableName, items);
        }
    }
    

    我创建了一个DbColumnAttribute,因此我们可以为类属性指定一个特定的列名。

    public sealed class DbColumnAttribute : Attribute
    {
        public string Name { get; set; }
    
        public DbColumnAttribute(string name)
        {
            Name = name;
        }
    }
    

    我为CreateTableIfNotExistsInsertAll 方法添加了一些IDbConnection 扩展。

    这很粗糙,所以我没有正确映射类型

    internal static class DbConnectionExtensions
    {
        public static void CreateTableIfNotExists<T>(this IDbConnection connection, string tableName)
        {
            var columns = GetColumnsForType<T>();
            var fields = string.Join(", ", columns.Select(x => $"[{x.Item1}] TEXT"));
            var sql = $"CREATE TABLE IF NOT EXISTS [{tableName}] ({fields})";
    
            ExecuteNonQuery(sql, connection);
        }
    
        public static void Insert<T>(this IDbConnection connection, string tableName, T item)
        {
            var properties = typeof(T)
                .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                .ToDictionary(x => x.Name, y => y.GetValue(item, null));
            var fields = string.Join(", ", properties.Select(x => $"[{x.Key}]"));
            var values = string.Join(", ", properties.Select(x => EnsureSqlSafe(x.Value)));
            var sql = $"INSERT INTO [{tableName}] ({fields}) VALUES ({values})";
    
            ExecuteNonQuery(sql, connection);
        }
    
        public static void InsertAll<T>(this IDbConnection connection, string tableName, IEnumerable<T> items)
        {
            foreach (var item in items)
                Insert(connection, tableName, item);
        }
    
        private static IEnumerable<Tuple<string, Type>> GetColumnsForType<T>()
        {
            return from pinfo in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
                let attribute = pinfo.GetCustomAttribute<DbColumnAttribute>()
                let columnName = attribute?.Name ?? pinfo.Name
                select new Tuple<string, Type>(columnName, pinfo.PropertyType);
        }
    
        private static void ExecuteNonQuery(string commandText, IDbConnection connection)
        {
            using (var com = connection.CreateCommand())
            {
                com.CommandText = commandText;
                com.ExecuteNonQuery();
            }
        }
    
        private static string EnsureSqlSafe(object value)
        {
            return IsNumber(value)
                ? $"{value}"
                : $"'{value}'";
        }
    
        private static bool IsNumber(object value)
        {
            var s = value as string ?? "";
    
            // Make sure strings with padded 0's are not passed to the TryParse method.
            if (s.Length > 1 && s.StartsWith("0"))
                return false;
    
            return long.TryParse(s, out long l);
        }
    }
    

    您仍然可以像@Mikhail 在第 3 步中提到的那样使用它。

    【讨论】:

      【解决方案3】:

      我想就这个问题添加另一种观点,以及采用不同方法解决它的解决方案。

      Dapper 可以被视为对存储库类的依赖,因为它是我们无法控制的外部代码库。因此,测试它实际上并不属于单元测试的职责范围(更符合您提到的集成测试)。

      话虽如此,我们不能真正直接模拟 Dapper,因为它实际上只是在 IDbConnection 接口上设置的扩展方法。我们可以模拟所有 System.Data 代码,直到我们到达 IDbCommand Dapper 真正发挥作用的地方。然而,这将是很多工作,并且在大多数情况下不值得努力。

      我们可以创建一个简单的IDapperCommandExecutor 可模拟接口:

      
      public interface IDapperCommandExecutor
      {
          IDbConnection Connection { get; }
      
          T Query<T>(string sql, object? parameters = null);
      
          // Add other Dapper Methods as required...
      }
      
      

      这个接口可以简单地用 Dapper 实现:

      
      public class DapperCommandExecutor : IDapperCommandExecutor
      {
          public DapperCommandExecutor(IDbConnection connection)
          {
              Connection = connection;
          }
      
          IDbConnection Connection { get; }
      
          T Query<T>(string sql, object? parameters = null) 
              => Connection.QueryAsync<T>(sql, parameters);
      
          // Add other Dapper Methods as required...
      }
      
      

      那么您所要做的就是更改以下内容:

      var queryResult = sqlConn.Query<T>(query.RawSql, query.Parameters);
      

      var queryResult = commandExecutor.Query<T>(query.RawSql, query.Parameters);
      

      然后在你的测试中,你可以创建一个模拟的命令执行器

      
      public class MockCommandExecutor : Mock<IDapperCommandExecutor>
      {
      
          public MockCommandExecutor()
          {
              // Add mock code here...
          }
      
      }
      
      

      总而言之,我们不需要测试 Dapper 库,它可以在单元测试中被模拟。这个模拟的 Dapper 命令执行器将减少对内存数据库的额外依赖要求,并可以降低复杂性你的测试。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-07-06
        • 2016-05-30
        • 2015-03-23
        • 2021-05-16
        • 2011-05-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多