2010-08-11 21 views
49

Estoy intentando controlar las pruebas de Unit una muy sencilla aplicación de prueba ASP.NET MVC que he creado usando el enfoque Code First en el último EF4 CTP. No soy muy experiencia con pruebas unitarias/burlarse etc.Pruebas unitarias con EF4 "Code First" y Repository

Ésta es mi clase de repositorio:

public class WeightTrackerRepository 
{ 
    public WeightTrackerRepository() 
    { 
     _context = new WeightTrackerContext(); 
    } 

    public WeightTrackerRepository(IWeightTrackerContext context) 
    { 
     _context = context; 
    } 

    IWeightTrackerContext _context; 

    public List<WeightEntry> GetAllWeightEntries() 
    { 
     return _context.WeightEntries.ToList(); 
    } 

    public WeightEntry AddWeightEntry(WeightEntry entry) 
    { 
     _context.WeightEntries.Add(entry); 
     _context.SaveChanges(); 
     return entry; 
    } 
} 

Ésta es IWeightTrackerContext

public interface IWeightTrackerContext 
{ 
    DbSet<WeightEntry> WeightEntries { get; set; } 
    int SaveChanges(); 
} 

... y su aplicación, WeightTrackerContext

public class WeightTrackerContext : DbContext, IWeightTrackerContext 
{ 
    public DbSet<WeightEntry> WeightEntries { get; set; } 
} 

En mi prueba, tengo lo siguiente:

[TestMethod] 
public void Get_All_Weight_Entries_Returns_All_Weight_Entries() 
{ 
    // Arrange 
    WeightTrackerRepository repos = new WeightTrackerRepository(new MockWeightTrackerContext()); 

    // Act 
    List<WeightEntry> entries = repos.GetAllWeightEntries(); 

    // Assert 
    Assert.AreEqual(5, entries.Count); 
} 

Y mi MockWeightTrackerContext:

class MockWeightTrackerContext : IWeightTrackerContext 
{ 
    public MockWeightTrackerContext() 
    { 
     WeightEntries = new DbSet<WeightEntry>(); 
     WeightEntries.Add(new WeightEntry() { Date = DateTime.Parse("01/06/2010"), Id = 1, WeightInGrams = 11200 }); 
     WeightEntries.Add(new WeightEntry() { Date = DateTime.Parse("08/06/2010"), Id = 2, WeightInGrams = 11150 }); 
     WeightEntries.Add(new WeightEntry() { Date = DateTime.Parse("15/06/2010"), Id = 3, WeightInGrams = 11120 }); 
     WeightEntries.Add(new WeightEntry() { Date = DateTime.Parse("22/06/2010"), Id = 4, WeightInGrams = 11100 }); 
     WeightEntries.Add(new WeightEntry() { Date = DateTime.Parse("29/06/2010"), Id = 5, WeightInGrams = 11080 }); 
    } 

    public DbSet<WeightEntry> WeightEntries { get;set; } 

    public int SaveChanges() 
    { 
     throw new NotImplementedException(); 
    } 
} 

Mi problema se produce cuando estoy tratando de construir algunos datos de prueba ya que no puedo crear un DbSet<> ya que no tiene constructor. Me da la sensación de que estoy ladrando al árbol equivocado con todo mi enfoque tratando de burlarme de mi contexto. Cualquier consejo sería bienvenido a este novato de pruebas unitarias completas.

Respuesta

48

Crea un DbSet a través del método Factory Set() en el contexto pero no desea ninguna dependencia de EF en la prueba de su unidad. Por lo tanto, lo que debe observar es implementar un stub de DbSet utilizando la interfaz IDbSet o un Stub usando uno de los frameworks Mocking como Moq o RhinoMock. Suponiendo que haya escrito su propio Stub, simplemente agregará los objetos WeightEntry a un hashset interno.

Puede tener más suerte aprendiendo sobre pruebas unitarias EF si busca ObjectSet y IObjectSet. Estas son las contrapartidas de DbSet antes del primer lanzamiento del CTP y tienen mucho más escrito sobre ellas desde una perspectiva de prueba unitaria.

Aquí hay un excellent article en MSDN que analiza la capacidad de prueba del código EF. Utiliza IObjectSet, pero creo que sigue siendo relevante.

Como respuesta al comentario de David, estoy agregando este apéndice a continuación, ya que no cabría en los comentarios. ¿No estoy seguro si esta es la mejor práctica para respuestas de comentarios largos?

Debe cambiar la interfaz IWeightTrackerContext para devolver un IDbSet de la propiedad WeightEntries en lugar de un tipo concreto de DbSet. A continuación, puede crear un MockContext con un marco de burla (recomendado) o con su propio talón personalizado. Esto devolvería un StubDbSet de la propiedad WeightEntries.

Ahora también tendrá un código (es decir, repositorios personalizados) que dependen del IWeightTrackerContext que en producción pasaría en su Entity Framework WeightTrackerContext que implementaría IWeightTrackerContext. Esto tiende a hacerse a través de la inyección de constructor utilizando un marco IoC como Unity. Para probar el código del repositorio que depende de EF, pasaría la implementación de MockContext para que el código bajo prueba piense que está hablando con la base de datos y la base de datos "real" y se comporta (con suerte) como se esperaba. Como ha eliminado la dependencia del sistema DB externo modificable y EF, puede verificar de manera fiable las llamadas al repositorio en las pruebas de su unidad.

Una gran parte de los frameworks de burlas proporciona la capacidad de verificar las llamadas en objetos de prueba para probar el comportamiento.En su ejemplo anterior, su prueba solo está probando la funcionalidad Agregar DbSet, que no debería ser su problema, ya que MS tendrá pruebas unitarias para eso. Lo que desearía saber es que la llamada al Add on DbSet se realizó desde su propio código de repositorio, si corresponde, y es ahí donde entran los frameworks de Mock.

Lo siento, sé que esto es mucho para digerir, pero si lees ese artículo, mucho se aclarará, ya que Scott Allen es mucho mejor para explicar esto que yo :)

+0

Gracias por el enlace, que se ve un gran artículo. Con respecto a la implementación de un stub de DbSet, ¿está diciendo que mi MockWeightTrackerContext devolvería, digamos, MyDbSetStub en lugar de DbSet? Pero esa clase no implementaría la interfaz. Perdón por las preguntas (probablemente tontas). ;) – DavidGouge

+0

Muchas gracias por tomarse el tiempo para explicar esto. Este y el enlace que proporcionó han ayudado mucho. Si pudiera votar su respuesta nuevamente, lo haría. : D – DavidGouge

+0

Gracias, ese artículo de MSDN que implementó InMemoryObjectSet fue justo lo que necesito para que mi IContext funcione correctamente. – kamranicus

41

Sobre la base de lo que dijo Daz, (y como mencioné en sus comentarios), van a necesitar hacer una implementación 'falsa' de IDbSet. Puede encontrar un ejemplo para CTP 4 here pero para que funcione, deberá personalizar su método Find y agregar valores devueltos para algunos de los métodos anteriormente anulados, como Agregar.

La siguiente es mi propio ejemplo diseñado para CTP 5:

public class InMemoryDbSet<T> : IDbSet<T> where T : class 
{ 
    readonly HashSet<T> _data; 
    readonly IQueryable _query; 

    public InMemoryDbSet() 
    { 
     _data = new HashSet<T>(); 
     _query = _data.AsQueryable(); 
    } 

    public T Add(T entity) 
    { 
     _data.Add(entity); 
     return entity; 
    } 

    public T Attach(T entity) 
    { 
     _data.Add(entity); 
     return entity; 
    } 

    public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T 
    { 
     throw new NotImplementedException(); 
    } 

    public T Create() 
    { 
     return Activator.CreateInstance<T>(); 
    } 

    public virtual T Find(params object[] keyValues) 
    { 
     throw new NotImplementedException("Derive from FakeDbSet and override Find"); 
    } 

    public System.Collections.ObjectModel.ObservableCollection<T> Local 
    { 
     get { return new System.Collections.ObjectModel.ObservableCollection<T>(_data); } 
    } 

    public T Remove(T entity) 
    { 
     _data.Remove(entity); 
     return entity; 
    } 

    public IEnumerator<T> GetEnumerator() 
    { 
     return _data.GetEnumerator(); 
    } 

    IEnumerator IEnumerable.GetEnumerator() 
    { 
     return _data.GetEnumerator(); 
    } 

    public Type ElementType 
    { 
     get { return _query.ElementType; } 
    } 

    public Expression Expression 
    { 
     get { return _query.Expression; } 
    } 

    public IQueryProvider Provider 
    { 
     get { return _query.Provider; } 
    } 
} 
+3

Clase útil. ¡Gracias por compartir! –

+0

Esta clase fue muy útil, ¡gracias! – JMGH

+1

Podría ser aún más útil, si proporciona delegado para Buscar en el constructor: de lectura privada Func , T> _finder; public InMemoryDbSet (Func , T> finder) { _finder = finder; _data = new HashSet (); _query = _data.AsQueryable(); } y pública t encontramos virtual (params Object []) KeyValues ​​ { retorno _finder (keyValues, _data); } –

0

Es probable conseguir un error

AutoFixture was unable to create an instance from System.Data.Entity.DbSet`1[...], most likely because it has no public constructor, is an abstract or non-public type. 

El DbSet tiene un constructor protegido.

El enfoque general se toma para apoyar la capacidad de prueba a crear su propia implementación de la Db Conjunto

public class MyDbSet<T> : IDbSet<T> where T : class 
{ 

utilizar esto como

public class MyContext : DbContext 
{ 
    public virtual MyDbSet<Price> Prices { get; set; } 
} 

Si nos fijamos en la cuestión por debajo de OE, 2ns respuesta, usted debe ser capaz de encontrar la implementación completa de un DbSet personalizado.

Unit testing with EF4 "Code First" and Repository

Entonces el

var priceService = fixture.Create<PriceService>(); 

no debe lanzar una excepción.