我知道有几个类似于我的问题。
但我不认为上述问题都有符合我要求的明确答案。
现在我开发了一个新的WebAPI项目,并在WebAPI项目和DataAccess技术之间进行了划分。我没有问题测试Controller for 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
}
答案 0 :(得分:21)
以下是我们的方法:
首先,您需要在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");
}
}
您的测试将创建一个包含一些示例行的内存数据库,并检查存储库如何检索它们:
[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);
}
我想有多种方法可以实现这样的内存数据库;我们在OrmLite
数据库上使用了SQLite
:
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);
}
}
}
}
答案 1 :(得分:1)
我改编了@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
,因此我们可以为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<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);
}
}
您仍然可以像在步骤3中提到的@Mikhail一样使用它。
答案 2 :(得分:0)
我想对这个问题和解决方案采取另一种观点,采用不同的方法来解决它。
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);
然后在测试中,您可以创建模拟的Command Executor
public class MockCommandExecutor : Mock<IDapperCommandExecutor>
{
public MockCommandExecutor()
{
// Add mock code here...
}
}
总而言之,我们不需要测试Dapper库,可以对它进行模拟,以进行单元测试。此模拟的Dapper Command Executor可以减少内存数据库的附加依赖项要求,并可以降低Dapper库的复杂性。您的测试。