使用内联查询进行单元测试
本文关键字:单元测试 查询 | 更新日期: 2023-09-27 17:50:42
我知道有几个问题和我的相似。
- Dapper: Unit Testing SQL Queries
- 测试文件查询
但我认为以上两个问题都没有明确的答案符合我的要求。
现在我正在开发一个新的WebAPI项目,并将其分为WebAPI项目和DataAccess技术。我没有问题测试控制器的WebAPI,因为我可以模拟数据访问类。
但是对于DataAccess类,这是一个不同的故事,因为我使用Dapper在它的内联查询,我有点困惑我如何通过使用单元测试来测试它。我问过我的一些朋友,他们更喜欢做集成测试而不是单元测试。
我想知道的是,是否有可能对使用Dapper和内联查询的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
}
我们的方法是:
-
首先,您需要在
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); }
-
我想有多种方法来实现这样的内存数据库;我们在
SQLite
数据库之上使用OrmLite
: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); } } } }
我改编了@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
,所以我们可以为class属性指定一个特定的列名。
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);
}
}
你仍然可以像@Mikhail在步骤3中提到的那样使用它
我想从另一个角度来看待这个问题,并从另一个角度来解决这个问题。
Dapper可以被认为是对仓库类的依赖,因为它是一个我们无法控制的外部代码库。因此,测试它实际上不在单元测试的职责范围内(更符合您提到的集成测试)。
尽管如此,我们不能直接模拟Dapper,因为它实际上只是在IDbConnection
接口上设置的一个扩展方法。我们可以模拟整个系统。数据代码,直到我们到达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将减少对内存中数据库的额外依赖需求,并可以降低测试的复杂性。