C# 使用内联查询进行单元测试

C# 使用内联查询进行单元测试,c#,unit-testing,dapper,C#,Unit Testing,Dapper,我知道有几个问题与我的问题相似 但我不认为上述两个问题都有符合我要求的明确答案 现在我开发了一个新的WebAPI项目,并在WebAPI项目和DataAccess技术之间进行了拆分。我在测试WebAPI控制器时没有问题,因为我可以模拟数据访问类 但对于DataAccess类,这是一个不同的故事,因为我使用的是Dapper,其中包含内联查询,所以我有点困惑如何使用单元测试来测试它。我问过一些朋友,他们更喜欢做集成测试而不是单元测试 我想知道的是,是否可以对使用Dapper和内联查询的Data

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

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

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

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

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

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

公共抽象类存储库:SyncTwoWayXI,IRepository其中T:IDatabaseTable
{
公共虚拟IResult GetItem(字符串accountName,长id)
{
if(id p.CustomAttributes.All(a=>a.AttributeType!=typeof(SqlMapperExtensions.DapperIgnore)).Select(p=>p.Name));
建造商名称(T.名称的类型);
其中(“id=@id”,新的{id});
其中(“accountID=@accountID”,新的{accountID=accountName});
builder.Where(“state!=“DELETED”);
var result=新结果();
var queryResult=sqlConn.Query(Query.RawSql,Query.Parameters);
if(queryResult==null | |!queryResult.Any())
{
result.Message=“未找到数据”;
返回结果;
}
结果=新结果(queryResult.ElementAt(0));
返回结果;
}
//创建、更新和删除的代码
}
上述代码的实现如下

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
}
公共类ProductIndex:IDatabaseTable
{
[SqlMapperExtensions.DapperKey]
公共Int64 id{get;set;}
公共字符串accountID{get;set;}
公共字符串用户标识{get;set;}
公共字符串设备ID{get;set;}
公共字符串deviceName{get;set;}
公共Int64事务ID{get;set;}
公共字符串状态{get;set;}
公共日期时间上次更新{get;set;}
公共字符串代码{get;set;}
公共字符串说明{get;set;}
公共浮动利率{get;set;}
公共字符串{get;set;}
公共浮动成本{get;set;}
公共字符串类别{get;set;}
公共int?类型{get;set;}
}
公共类产品存储库:存储库
{
//..重写创建、更新、删除方法
}
以下是我们的方法:

  • 首先,您需要在
    IDbConnection
    之上有一个抽象来模拟它:

    public interface IDatabaseConnectionFactory
    {
        IDbConnection GetConnection();
    }
    
  • 您的存储库将从此工厂获得连接,并对其执行
    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");
        }
    }
    

  • 我调整了@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);
        }
    }
    
    我为
    CreateTableIfNotExists
    InsertAll
    方法添加了一些IDbConnection扩展

    这是非常粗糙的,所以我没有正确映射类型

    内部静态类DbConnectionExtensions
    {
    公共静态void CreateTableIfNotExists(此IDbConnection连接,字符串tableName)
    {
    var columns=GetColumnsForType();
    var fields=string.Join(“,”,columns.Select(x=>$”[{x.Item1}]TEXT”);
    var sql=$“如果不存在创建表[{tableName}]({fields})”;
    ExecuteOnQuery(sql,连接);
    }
    公共静态void Insert(此IDbConnection连接,字符串tableName,T项)
    {
    变量属性=类型(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=$“插入[{tableName}]({fields})值({VALUES})”;
    ExecuteOnQuery(sql,连接);
    }
    公共静态void InsertAll(此IDbConnection连接、字符串表名、IEnumerable项)
    {
    foreach(项目中的var项目)
    插入(连接、表名、项目);
    }
    私有静态IEnumerable GetColumnsForType()的
    {
    以typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)的形式从pinfo返回
    let attribute=pinfo.GetCustomAttribute()
    让columnName=属性?.Name??pinfo.Name
    选择新元组(columnName、pinfo.PropertyType);
    }
    私有静态void ExecuteNonQuery(字符串commandText,IDbConnection)
    {
    使用(var com=connection.CreateCommand())
    {
    com.CommandText=CommandText;
    com.ExecuteNonQuery();
    }
    }
    私有静态字符串EnsureSqlSafe(对象值)
    {
    返回IsNumber(值)
    ?$“{value}”
    :$“{value}”;
    }
    私有静态bool IsNumber(对象值)
    {
    var s=字符串形式的值??“”;
    //确保带填充0的字符串未传递给TryParse方法。
    如果(s.Length>1和s.StartsWith(“0”))
    返回false;
    返回长。锥虫(s,出长l);
    }
    }
    

    您仍然可以按照@Mikhail在第3步中提到的方法使用它。

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

    Dapper可以被视为对repository类的依赖,因为它是一个我们无法控制的外部代码库。因此,测试它并不真正属于单元测试的责任范围(M
    [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);
    }
    
    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);
                }
            }
        }
    }
    
    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);
        }
    }
    
    public sealed class DbColumnAttribute : Attribute
    {
        public string Name { get; set; }
    
        public DbColumnAttribute(string name)
        {
            Name = name;
        }
    }
    
    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);
        }
    }
    
    
    public interface IDapperCommandExecutor
    {
        IDbConnection Connection { get; }
    
        T Query<T>(string sql, object? parameters = null);
    
        // Add other Dapper Methods as required...
    }
    
    
    
    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...
        }
    
    }