2011-09-02 9 views
6

Decidí comenzar a escribir pruebas unitarias en nuestra aplicación. Utiliza Entity Framework con un patrón de repositorio.Prueba de unidad EF Repositorio patrón con Moq

Ahora quiero comenzar a probar las clases de lógica que están utilizando los repositorios. Proporciono un ejemplo simple aquí.

Tres de mis métodos en el GenericRepository clase:

public class GenericRepository : IRepository 
{ 
    public IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class 
    { 
     var entityName = GetEntityName<TEntity>(); 
     return Context.CreateQuery<TEntity>(entityName); 
    } 
    private string GetEntityName<TEntity>() where TEntity : class 
    { 
     return typeof(TEntity).Name; 
    } 
    public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class 
    { 
     return GetQuery<TEntity>().Where(predicate).AsEnumerable(); 
    } 
} 

Una clase simple lógica regresando años distintos de una tabla de calendario en orden descendente (sí sé la palabra calendario está mal escrito en nuestro código):

public class GetDistinctYearsFromCalendar 
{ 
    private readonly IRepository _repository; 

    public GetDistinctYearsFromCalendar() 
    { 
     _repository = new GenericRepository(); 
    } 

    internal GetDistinctYearsFromCalendar(IRepository repository) 
    { 
     _repository = repository; 
    } 

    public int[] Get() 
    { 
     return _repository.Find<Calender_Tbl>(c => c.Year.HasValue).Select(c => c.Year.Value).Distinct().OrderBy(c => c).Reverse().ToArray(); 
    } 
} 

Y aquí es mi primera prueba:

[TestFixture] 
public class GetDistinctYearsFromCalendarTest 
{ 
    [Test] 
    public void ReturnsDistinctDatesInCorrectOrder() 
    { 
     var repositoryMock = new Mock<IRepository>(); 

     repositoryMock.Setup(r => r.Find<Calender_Tbl>(c => c.Year.HasValue)).Returns(new List<Calender_Tbl> 
     { 
      new Calender_Tbl 
       { 
        Date = 
         new DateTime(2010, 1, 1), 
        Year = 2010 
       }, 
      new Calender_Tbl 
       { 
        Date = 
         new DateTime(2010, 2, 1), 
        Year = 2010 
       }, 
      new Calender_Tbl 
       { 
        Date = 
         new DateTime(2011, 1, 1), 
        Year = 2011 
       } 
     }.AsQueryable()); 

     var getDistinct = new GetDistinctYearsFromCalendar(repositoryMock.Object).Get(); 

     Assert.AreEqual(2, getDistinct.Count(), "Returns more years than distinct."); 
     Assert.AreEqual(2011, getDistinct[0], "Incorrect order, latest years not first."); 
     Assert.AreEqual(2010, getDistinct[1], "Wrong year."); 


    } 
} 

Este está funcionando bien Pero esto no es realmente lo que quiero hacer. Como tengo que configurar el método Buscar en el objeto simulado, también necesito saber cómo se llamará en mi clase lógica. Si me gustaría hacer TDD, no quiero que me importe esto. Todo lo que quiero saber es qué entidades de calendario debería proporcionar mi repositorio. Me gustaría configurar el método GetQuery. De esta manera:

repositoryMock.Setup(r => r.GetQuery<Calender_Tbl>()).Returns(new List<Calender_Tbl> 
{ 
    new Calender_Tbl 
     { 
      Date = 
       new DateTime(2010, 1, 1), 
      Year = 2010 
     }, 
    new Calender_Tbl 
     { 
      Date = 
       new DateTime(2010, 2, 1), 
      Year = 2010 
     }, 
    new Calender_Tbl 
     { 
      Date = 
       new DateTime(2011, 1, 1), 
      Year = 2011 
     } 
}.AsQueryable()); 

Así que cuando Find se llama GetQuery internamente en la clase GenericRepository que deben recibir las entidades calendario correcto que tengo que instalar en GetQuery. Pero esto no está funcionando, por supuesto. Como no configuré el método Buscar de mi objeto falso, no obtengo ninguna entidad.

¿Qué hacer? Por supuesto, probablemente podría usar Moles o algún otro framework que se burle de todo, pero no quiero hacer eso. ¿Hay algo que pueda hacer en el diseño de la clase o prueba para resolver el problema?

No es el fin del mundo si tengo que ir con mi solución actual, pero ¿qué pasa si el año de la propiedad se convierte en una int no nulable? Entonces, por supuesto, tendré que cambiar mi implementación en la clase lógica, pero también tendría que cambiar la prueba. Me gustaría tratar de evitar esto.

+1

Creo que una forma de hacerlo es hacer que 'GetDistinctYearsFromCalendar' dependa de 'IRepository'. De esta forma, puede burlarse del 'GenericRepository' una prueba 'GetDistinctYearsFromCalendar' aislada. Consulte http://en.wikipedia.org/wiki/Dependency_injection para obtener más detalles. – Michael

+0

Está bien, no vi el ctor interno. ¿Tienes pruebas y código de producción en el mismo proyecto? Creo que la forma correcta de hacerlo es haciendo la inyección de dependencia por completo y omitir el ctor predeterminado. – Michael

+0

Me doy cuenta de que podría simplemente escribir mi lógica como: return _repository.GetQuery (). Where (c => c.Year.HasValue) .Seleccione (c => c.Year.Value) .Distinct(). OrderBy (c => c) .Reverse(). ToArray(); No Sigo las pruebas en otro proyecto pero expongo las partes internas del proyecto de prueba con las directivas de ensamblaje. ¿Pero qué pasa si nunca uso otro tipo de GenericRepository? ¿Es necesario hacer una inyección de dependencia en todo momento? Esa parece ser la mejor solución ¿verdad? Estaba ciego y quería usar toda la funcionalidad del repositorio. – John

Respuesta

12

puedo ver dos maneras:

public class MockRepository : IRepository 
{ 
    private List<object> entities; 
    public MockRepository(params object[] entitites) 
    { 
     this.entities = entities.ToList(); 
    } 

    public IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class 
    { 
     return this.entities.OfType<TEntity>().AsQueryable(); 
    } 

    public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class 
    { 
     return GetQuery<TEntity>().Where(predicate).AsEnumerable(); 
    } 
} 

Esa es la más fácil y mi forma preferida. Moq no es el martillo para todo;)

Alternativamente, si realmente insiste en usar Moq (me siento halagado, pero en este caso es muy innecesario, ya que puede hacer pruebas basadas en estado en las entidades devueltas) , que puede hacer:

public class GenericRepository : IRepository 
{ 
    public virtual IQueryable<TEntity> GetQuery<TEntity>() where TEntity : class 
    { 
     var entityName = GetEntityName<TEntity>(); 
     return Context.CreateQuery<TEntity>(entityName); 
    } 
    private string GetEntityName<TEntity>() where TEntity : class 
    { 
     return typeof(TEntity).Name; 
    } 
    public IEnumerable<TEntity> Find<TEntity>(Expression<Func<TEntity, bool>> predicate) where TEntity : class 
    { 
     return GetQuery<TEntity>().Where(predicate).AsEnumerable(); 
    } 
} 

y luego usar Moq para anular el comportamiento de GetQuery:

var repository = new Mock<GenericRepository> { CallBase = true }; 

repository.Setup(x => x.GetQuery<Foo>()).Returns(theFoos.AsQueryable()); 

lo que sucederá es que el método Find conseguirá ejecutado en la clase GenericRepository, que a su vez GetQuery, que ha sido sobrescrito por Moq para provi del conjunto fijo de entidades.

Configuré CallBase = true explícitamente en caso de que también realice Buscar virtual, para asegurarnos de que siempre se invoca. No es técnicamente necesario si Find no es virtual, ya que siempre se invocará en la clase real de la que el simulador está heredando/burlando.

Me gustaría ir por la primera opción, mucho más simple de entender lo que está pasando, y puede reutilizarse fuera del contexto de una única prueba en particular (simplemente pase las entidades que necesita y funcionará para todo).

+0

Como entendí que burlarse de un método que devuelve IQueryable no fue comprobable porque debe ejecutar una prueba de integración con la base de datos, me pregunto si es posible simular el método Buscar y hacer que devuelva mis entidades independientemente de los argumentos que apruebe. Intenté con It.IsAny <> pero no funcionó. – John

0

Recientemente, ha surgido una nueva herramienta llamada Effort para EF 6+ que me pareció tremendamente útil para las pruebas unitarias contra una base de datos falsa. Ver http://effort.codeplex.com/wikipage?title=Tutorials&referringTitle=Home.

Añádelo mediante el uso de este paquete de comando de la consola de administrador:

PM> Install-Package Effort.EF6 

Luego agregar una interfaz para su DbContext, por ejemplo, si está utilizando la base de datos AdventureWorks (ver https://sql2012kitdb.codeplex.com/):

A continuación, actualizar su DbContext para añadir dos nuevos constructores parametrizados:

/// 
    /// Create a new context based on database name or connection string. 
    /// 
    /// Database name or connection string 
    public AdventureWorksEntities(string nameOrConnectionString) 
     : base(nameOrConnectionString) 
    { 
     this.Configuration.LazyLoadingEnabled = false; 
    } 

    public AdventureWorksEntities(DbConnection connection) 
     : base(connection, true) 
    { 
     this.Configuration.LazyLoadingEnabled = false; 
    } 

añadir un constructor que toma la interfaz a su repositorio:

private IAdventureWorksDbContext _dbContext; 

    public ProductRepository(IAdventureWorksDbContext dbContext) 
    { 
     dbContext.Configuration.AutoDetectChangesEnabled = false; 
     this._dbContext = dbContext; 
    } 

A continuación, añadir una interfaz para su proyecto de pruebas unitarias y de clase asociado:

public interface ITestDatabase : IDisposable 
{ 
    IAdventureWorksDbContext CreateContext(); 

    void Dispose(IAdventureWorksDbContext context); 
} 

Añadir algunos datos falsos para su proyecto de prueba de unidad:

public class ProductsTestData 
{ 
    public static void AddTestData(IAdventureWorksDbContext dbContext) 
    { 
     dbContext.Products.Add(new Product() { Id = new Guid("23ab9e4e-138a-4223-bb42-1dd176d8583cB"), Name = "Product A", CreatedDate = DateTime.Now, Description = "Product description..." }); 
     dbContext.Products.Add(new Product() { Id = new Guid("97e1835f-4c1b-4b87-a514-4a17c019df00"), Name = "Product B", CreatedDate = DateTime.Now }); 
     dbContext.SaveChanges(); 
    } 
} 

Ahora la configuración de su clase de pruebas unitarias :

[TestClass] 
public class ProductsTest 
{ 
    private ITestDatabase _testDatabaseStrategy; 
    private ProductRepository _productRepository; 
    private IAdventureWorksDbContext _context; 

    [TestInitialize] 
    public void SetupTest() 
    { 
     // create the test strategy. This will initialise a new database 
     _testDatabaseStrategy = CreateTestStrategy(); 

     // add test data to the database instance 
     using (_context = _testDatabaseStrategy.CreateContext()) 
     { 
      ProductsTestData.AddTestData(_context); 
      _context.SaveChanges(); 
     } 

     // initialise the repository we are testing 
     _context = _testDatabaseStrategy.CreateContext(); 
     _productRepository = new ProductRepository(_context); 
    } 

    protected ITestDatabase CreateTestStrategy() 
    { 
     return new EffortDatabaseStrategy(); 
    } 

    [TestCleanup] 
    public void CleanupTest() 
    { 
     // dispose of the database and connection 
     _testDatabaseStrategy.Dispose(_context); 
     _context = null; 
    } 

    [TestMethod] 
    public void GetProductsByTagName() 
    { 
     IEnumerable<Product> products = _productRepository.GetProductsByTagName("Tag 1", false); 
     Assert.AreEqual(1, products.Count()); 
    } 

Wh ERE EffortDatabaseStrategy es:

public class EffortDatabaseStrategy : ITestDatabase 
{ 
    public EffortDatabaseStrategy() 
    { 
    } 

    private DbConnection _connection; 

    public IAdventureWorksDbContext CreateContext() 
    { 
     if (_connection == null) 
     { 
      _connection = Effort.DbConnectionFactory.CreateTransient(); 
     } 
     var context = new AdventureWorksDbContext(_connection); 

     return context; 
    } 

    public void Dispose(IAdventureWorksDbContext context) 
    { 
     if (context != null) 
     { 
      context.Dispose(); 
     } 
    } 

    public void Dispose() 
    { 
    } 
} 

Para más detalles, consulte http://www.codeproject.com/Articles/460175/Two-strategies-for-testing-Entity-Framework-Effort?msg=5122027#xx5122027xx.

Cuestiones relacionadas