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 }
这是我们的方法:
-
首先,你需要在
IDbConnection
之上有一个抽象来模拟它:public interface IDatabaseConnectionFactory { IDbConnection GetConnection(); }
-
您的存储库将从此工厂获取连接并对其执行
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"); } } -
您的测试将创建一个包含一些示例行的内存数据库,并检查存储库如何检索它们:
[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); } -
我想有多种方法可以实现这样的内存数据库; 我们在
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; } }
我为CreateTableIfNotExists
和InsertAll
方法添加了一些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一样使用它。