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