unit testingDapper与内联查询

我知道有几个类似于我的问题。

  • Dapper:unit testingSQL查询
  • 测试Dapper查询

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

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

但对于DataAccess类来说,这是一个不同的故事,因为我在其中使用Dapper内联查询,我有点混淆我如何使用unit testing来测试它。 我问了一些朋友,他们更喜欢做集成测试而不是unit testing。

我想知道的是,是否可以对使用Dapper和Inline查询的DataAccess类进行unit testing。

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

public abstract class Repository : SyncTwoWayXI, IRepository where T : IDatabaseTable { public virtual IResult GetItem(String accountName, long id) { if (id  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(); var queryResult = sqlConn.Query(query.RawSql, query.Parameters); if (queryResult == null || !queryResult.Any()) { result.Message = "No Data Found"; return result; } result = new Result(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 { // ..override Create, Update, Delete method } 

这是我们的方法:

  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> GetAll() { return this.connectionFactory.GetConnection().QueryAsync( "select * from Product"); } } 
  3. 您的测试将创建一个包含一些示例行的内存数据库,并检查存储库如何检索它们:

     [Test] public async Task QueryTest() { // Arrange var products = new List { 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(IEnumerable items) { using (var db = this.OpenConnection()) { db.CreateTableIfNotExists(); foreach (var item in items) { db.Insert(item); } } } } 

我改编了@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(string tableName, IEnumerable items) { var con = OpenConnection(); con.CreateTableIfNotExists(tableName); con.InsertAll(tableName, items); } } 

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

 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(this IDbConnection connection, string tableName) { var columns = GetColumnsForType(); 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(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(this IDbConnection connection, string tableName, IEnumerable items) { foreach (var item in items) Insert(connection, tableName, item); } private static IEnumerable> GetColumnsForType() { return from pinfo in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance) let attribute = pinfo.GetCustomAttribute() let columnName = attribute?.Name ?? pinfo.Name select new Tuple(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); } } 

你仍然可以像在步骤3中提到的@Mikhail一样使用它。