web-dev-qa-db-fra.com

Test unitaire de Dapper avec des requêtes en ligne

Je sais qu'il y a plusieurs questions semblables aux miennes.

mais je ne pense pas que les deux questions ci-dessus aient une réponse claire qui corresponde à mes exigences.

Actuellement, je développe un nouveau projet WebAPI et le partage entre le projet WebAPI et la technologie DataAccess. Je n'ai pas de problème à tester le contrôleur pour WebAPI puisque je peux me moquer de la classe d'accès aux données.

Mais pour la classe DataAccess, c'est une histoire différente, puisque j'utilise Dapper avec des requêtes en ligne, je suis un peu confus, comment puis-je le tester en utilisant Unit Test. J'ai demandé à certains de mes amis et ils préfèrent faire le test d'intégration plutôt que le test unitaire.

Ce que je veux savoir, c'est s'il est possible de tester à l'unité la classe DataAccess qui utilise les requêtes Dapper et Inline.

Disons que j'ai une classe comme celle-ci (il s'agit d'une classe de référentiel générique, car beaucoup de codes ont des requêtes similaires différenciées par le nom de la table et le champ)

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
  }

Et la mise en œuvre pour le code ci-dessus est comme

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
}
15
Martin Valentino

Voici notre approche:

  1. Tout d'abord, vous devez avoir une abstraction sur IDbConnection pour pouvoir vous en moquer:

    public interface IDatabaseConnectionFactory
    {
        IDbConnection GetConnection();
    }
    
  2. Votre référentiel obtiendrait la connexion de cette fabrique et exécuterait la requête Dapper sur celle-ci:

    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");
        }
    }
    
  3. Votre test créerait une base de données en mémoire avec quelques exemples de lignes et vérifierait comment le référentiel les récupère:

    [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);
    }
    
  4. J'imagine qu'il y a plusieurs façons d'implémenter une telle base de données en mémoire; nous avons utilisé OrmLite au-dessus de la base de données 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);
                }
            }
        }
    }
    
20
Mikhail Shilkov

J'ai adapté ce que @ Mikhail a fait parce que j'avais des problèmes lors de l'ajout des packages 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);
    }
}

J'ai créé une DbColumnAttribute afin que nous puissions spécifier un nom de colonne spécifique pour une propriété de classes.

public sealed class DbColumnAttribute : Attribute
{
    public string Name { get; set; }

    public DbColumnAttribute(string name)
    {
        Name = name;
    }
}

J'ai ajouté des extensions IDbConnection pour les méthodes CreateTableIfNotExists et InsertAll

C'est très difficile donc je n'ai pas mappé les types correctement

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);
    }
}

Vous pouvez toujours l'utiliser de la même manière que @Mikhail mentionne à l'étape 3.

1
mrstebo