2015-05-26 3 views
14

So che ci sono diverse domande simili alle mie.Dapper test unitario con query in linea

butI non credo sia di domanda di cui sopra ha una risposta chiara che si adattano la mia richiesta.

In questo momento sviluppo un nuovo progetto WebAPI e divido tra il progetto WebAPI e la tecnologia DataAccess. Non ho problemi a testare il controller per WebAPI dato che posso prendere in giro la classe di accesso ai dati.

Ma per la classe DataAccess che è una storia diversa, dal momento che sto usando Dapper con query inline in esso, sono un po 'confuso come posso testarlo usando Unit Test. Ho chiesto ad alcuni dei miei amici e preferiscono fare test di integrazione invece di Unit Test.

Quello che voglio sapere è, è possibile testare la classe DataAccess che usa Dapper e le query Inline in esso.

Diciamo che ho una classe come questo (questa è una classe repository generico, in quanto molti dei codici hanno query simili differenziarsi dal nome della tabella e di campo)

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 
    } 

e l'implementazione di codice di cui sopra è come

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 
} 

risposta

11

Ecco il nostro approccio:

  1. Prima di tutto, è necessario avere un'astrazione in cima IDbConnection per essere in grado di prendere in giro è:

    public interface IDatabaseConnectionFactory 
    { 
        IDbConnection GetConnection(); 
    } 
    
  2. Il repository otterrebbe il collegamento da questa fabbrica ed eseguire la query Dapper su di esso:

    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. Il test sarebbe creare un database in-memory con alcune righe di esempio e verificare come il repository li recupera:

    [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. Suppongo che ci siano molti modi per implementare tale database in memoria; abbiamo usato OrmLite in cima SQLite database:

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

Solo una cosa, la fabbrica non è necessario ottenere un'astrazione di 'IDbConnection' (è già un'interfaccia), ma essere in grado di costruire nuove connessioni all'interno del repository. Se non è necessario (e probabilmente non è necessario creare più di una connessione nel contesto di una richiesta API Web) è possibile passare direttamente 'IDbConnection' al repository. –

+1

@IgnacioCalvo Abbiamo bisogno di più di una connessione allo stesso tempo, ad es. quando si eseguono query asincrone che possono essere eseguite in parallelo. – Mikhail

0

mi sono adattato quello @Mikhail fatto perché ho avuto problemi quando si aggiungono i pacchetti 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); 
    } 
} 

Ho creato un DbColumnAttribute in modo che possiamo specificare un nome di colonna specifica per una proprietà classi.

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

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

ho aggiunto alcune estensioni IDbConnection per i metodi CreateTableIfNotExists e InsertAll.

Questo è molto ruvida tipi quindi non ho mappato correttamente

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

è comunque possibile utilizzare allo stesso modo come @Mikhail menziona al punto 3.