2010-08-11 2 views
49

J'essaie de maîtriser l'unité de test d'une application de test ASP.NET MVC très simple que j'ai construite en utilisant l'approche Code First de la dernière version d'EF4 CTP. Je ne suis pas d'expérience très avec tests unitaires/moqueur, etc.Tests unitaires avec EF4 "Code First" et Repository

Ceci est ma classe Repository:

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

C'est IWeightTrackerContext

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

... et sa mise en œuvre, WeightTrackerContext

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

Dans mon test, j'ai ce qui suit:

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

Et mon 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(); 
    } 
} 

Mon problème se produit quand je suis en train de construire des données de test que je ne peux pas créer un DbSet<> car il n'a pas de constructeur. J'ai l'impression d'aboyer le mauvais arbre avec toute mon approche en essayant de me moquer de mon contexte. Tout conseil serait le bienvenu à ce débutant d'essai complet de l'unité.

Répondre

48

Vous créez un DbSet via la méthode Factory Set() sur le Context mais vous ne voulez pas de dépendance sur EF dans votre test unitaire. Par conséquent, ce que vous devez faire est de mettre en œuvre un bouchon de DbSet en utilisant l'interface IDbSet ou un Stub en utilisant l'un des frameworks Mocking tels que Moq ou RhinoMock. En supposant que vous ayez écrit votre propre Stub vous ajouteriez simplement les objets WeightEntry à un hashset interne.

Vous aurez peut-être plus de chance d'apprendre sur les tests unitaires EF si vous recherchez ObjectSet et IObjectSet. Ce sont les contreparties de DbSet avant la première version de CTP du code et ont beaucoup plus écrit à leur sujet dans une perspective de tests unitaires.

Voici un excellent article sur MSDN qui traite de la testabilité du code EF. Il utilise IObjectSet mais je pense que c'est toujours pertinent.

En réponse au commentaire de David, j'ajoute cet addendum ci-dessous car il ne rentre pas dans les commentaires. Vous ne savez pas si c'est la meilleure pratique pour les réponses à long commentaire?

Vous devez modifier l'interface IWeightTrackerContext pour renvoyer un IDbSet de la propriété WeightEntries plutôt qu'un type concret DbSet. Vous pouvez ensuite créer un MockContext avec un cadre de simulation (recommandé) ou votre propre talon personnalisé. Cela retournera un StubDbSet de la propriété WeightEntries.

Maintenant, vous aurez également du code (c'est-à-dire des dépôts personnalisés) qui dépendent du IWeightTrackerContext que vous auriez passé en production dans votre Entity Framework WeightTrackerContext qui implémenterait IWeightTrackerContext. Cela a tendance à être fait par injection de constructeur en utilisant un cadre IoC tel que Unity. Pour tester le code du référentiel qui dépend de EF, vous passerez votre implémentation MockContext de sorte que le code testé pense qu'il est en train de dialoguer avec le "vrai" EF et la base de données et qu'il se comporte comme prévu. Comme vous avez supprimé la dépendance sur le système db externe modifiable et EF, vous pouvez ensuite vérifier de manière fiable les appels du référentiel dans vos tests unitaires.

Une grande partie des cadres de simulation est la possibilité de vérifier les appels sur les objets Mock pour tester le comportement.Dans votre exemple ci-dessus, votre test ne fait que tester la fonctionnalité DbSet Add qui ne devrait pas vous concerner, MS ayant des tests unitaires pour cela. Ce que vous voudriez savoir, c'est que l'appel à Add on DbSet a été fait à partir de votre propre code de dépôt si c'est approprié et c'est là que les frameworks Mock entrent en jeu.

Désolé, je sais que c'est beaucoup à digérer mais Si vous avez lu cet article beaucoup deviendra plus clair que Scott Allen est beaucoup mieux à expliquer ce genre de choses que je suis :)

+0

Merci pour le lien, qui ressemble à un bon article. En ce qui concerne l'implémentation d'un talon de DbSet, dites-vous que mon MockWeightTrackerContext retournera, par exemple, MyDbSetStub au lieu de DbSet? Mais alors cette classe ne mettrait pas en œuvre l'interface. Désolé pour les questions (probablement stupides). ;) – DavidGouge

+0

Merci beaucoup d'avoir pris le temps d'expliquer cela. Ceci et le lien que vous avez fourni ont beaucoup aidé. Si je pouvais rejeter votre réponse à nouveau, je le ferais. : D – DavidGouge

+0

Merci, cet article MSDN implémentant le InMemoryObjectSet était exactement ce dont j'avais besoin pour que mon IContext fonctionne correctement. – kamranicus

41

En s'appuyant sur ce que dit Daz, (et comme je l'ai mentionné dans ses commentaires), vous vont avoir besoin de faire une «fausse» implémentation d'IDbSet. Un exemple pour CTP 4 peut être trouvé here mais pour le faire fonctionner, vous devrez personnaliser votre méthode Find et ajouter des valeurs de retour pour quelques-unes des méthodes précédemment annulées, comme Add.

Voici mon exemple reproduis à 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

Classe utile. Merci d'avoir partagé! –

+0

Cette classe a été très utile, merci! – JMGH

+1

Cela pourrait être encore plus utile, si vous fournissez un délégué pour Find dans le constructeur: private readonly Func , T> _finder; public InMemoryDbSet (Func , T> finder) { _finder = finder; _data = new HashSet (); _query = _data.AsQueryable(); } et Rechercher T virtuel public (objet params [] KeyValues) { de _finder de retour (keyValues, _data); } –

0

Vous obtenir probablement une erreur

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. 

Le DbSet a un constructeur protégé.

L'approche généralement pris pour soutenir testabilité créer votre propre implémentation de la Db Set

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

utiliser comme

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

Si vous regardez la question ci-dessous SO, 2 ns réponse, vous devriez être en mesure de trouver l'implémentation complète d'un DbSet personnalisé.

Unit testing with EF4 "Code First" and Repository

Puis le

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

ne le jetez pas une exception.