2015-05-26 11 views
14

Wiem, że jest kilka pytań podobnych do moich.Testowanie urządzenia z zapytaniami śródliniowymi

BUTI nie sądzę oba powyższe pytanie ma jasnej odpowiedzi, które pasują do mojego wymóg.

Teraz opracowuję nowy projekt WebAPI i dzielę go między projekt WebAPI i technologię DataAccess. Nie mam problemu z testowaniem kontrolera WebAPI, ponieważ mogę kpić z klasy dostępu do danych.

Ale dla klasy DataAccess są różne historie, ponieważ używam Dappera z wbudowanymi zapytaniami, jestem trochę zakłopotany, jak mogę przetestować to używając Testu Jednostki. Poprosiłem niektórych z moich znajomych i wolą wykonać test integracji zamiast testu jednostkowego.

Co chcę wiedzieć, czy możliwe jest przetestowanie klasy DataAccess, która używa w niej zapytań Dapper i Inline.

Powiedzmy mam klasy jak poniżej (jest to ogólna klasa repozytorium, ponieważ wiele z kodów mają podobne zapytania odróżnienia od nazwy tabeli i pola)

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 
    } 

i wdrożenie do powyższego kodu jest jak

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 
} 

Odpowiedz

11

Oto nasze podejście:

  1. Przede wszystkim, trzeba mają abstrakcji na szczycie IDbConnection aby móc go wyśmiewać:

    public interface IDatabaseConnectionFactory 
    { 
        IDbConnection GetConnection(); 
    } 
    
  2. Repozytorium by uzyskać połączenie z tej fabryki i wykonać zapytanie Dapper od niego:

    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. testu będzie utwórz bazę danych w pamięci z kilkoma przykładowymi wierszami i sprawdź, jak odzyskuje je repozytorium:

    [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. Sądzę, że istnieje wiele sposobów wdrożenia takiej bazy danych w pamięci; użyliśmy OrmLite na szczycie SQLite bazy danych:

    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

tylko jedna rzecz, fabryka nie jest konieczne, aby uzyskać abstrakcję 'IDbConnection' (jest to już interfejs), ale aby móc budować nowe połączenia wewnątrz repozytorium. Jeśli tego nie potrzebujesz (i prawdopodobnie nie będziesz musiał tworzyć więcej niż jednego połączenia w kontekście żądania Web API), możesz bezpośrednio przekazać "IDbConnection" do repozytorium. –

+1

@IgnacioCalvo Potrzebujemy więcej niż jednego połączenia w tym samym czasie, np. podczas wykonywania zapytań asynchronicznych, które mogą działać równolegle. – Mikhail

0

I dostosowany co @Mikhail zrobiłem, bo miałem problemy podczas dodawania pakietów 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); 
    } 
} 

Utworzyłem DbColumnAttribute więc możemy podać konkretną nazwę kolumny dla właściwości klas.

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

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

dodałem kilka rozszerzeń IDbConnection dla metod CreateTableIfNotExists i InsertAll.

Jest to bardzo niebezpieczne, więc nie zostały odwzorowane rodzaje poprawnie

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

Można nadal używać go w ten sam sposób jak @Mikhail wspomina w kroku 3.

Powiązane problemy