49 votes

Tests unitaires avec EF4 "Code First" et Repository

J'essaie d'obtenir une poignée sur le test unitaire d'une très simple ASP.NET MVC application de test que j'ai construit en utilisant l'approche Code First dans la dernière EF4 CTP. Je n'ai pas une grande expérience des tests unitaires, de la simulation, etc.

C'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 le IWeightTrackerContext.

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

...et son implémentation, WeightTrackerContext

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

Dans mon test, j'ai les éléments suivants :

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

Le problème se pose lorsque j'essaie de créer des données de test, car je n'arrive pas à créer une base de données. DbSet<> car il n'a pas de constructeur. J'ai l'impression de faire fausse route en essayant de simuler mon contexte. Tout conseil serait le bienvenu pour ce débutant en matière de tests unitaires.

48voto

Darren Lewis Points 5139

Vous créez un DbSet par la méthode Factory Set() sur le Contexte mais vous ne voulez pas de dépendance à EF dans votre test unitaire. Par conséquent, vous devez envisager d'implémenter un stub de DbSet en utilisant l'interface IDbSet ou un stub en utilisant l'un des frameworks de Mocking tels que Moq ou RhinoMock. En supposant que vous ayez écrit votre propre stub, vous ajouterez simplement les objets WeightEntry à un hashset interne.

Vous aurez peut-être plus de chance d'en savoir plus sur les tests unitaires EF si vous recherchez ObjectSet et IObjectSet. Ce sont les équivalents de DbSet avant la première version CTP du code et il y a beaucoup plus d'écrits à leur sujet du point de vue des 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 rentrerait pas dans les -commentaires. Je ne sais pas si c'est la meilleure pratique pour les longues réponses aux commentaires ?

Vous devez modifier l'interface IWeightTrackerContext pour retourner un IDbSet à partir de la propriété WeightEntries plutôt qu'un type concret DbSet. Vous pouvez ensuite créer un MockContext, soit avec un framework de mocking (recommandé), soit avec votre propre stub personnalisé. Celui-ci renverra un StubDbSet à partir de la propriété WeightEntries.

Maintenant, vous aurez également du code (c'est-à-dire des dépôts personnalisés) qui dépendent de l'IWeightTrackerContext. En production, vous passerez dans votre Entity Framework WeightTrackerContext qui implémentera l'IWeightTrackerContext. Cela se fait généralement par injection de constructeur en utilisant un framework IoC tel que Unity. Pour tester le code du référentiel qui dépend de EF, vous passerez dans votre implémentation de MockContext pour que le code testé pense qu'il parle au "vrai" EF et à la base de données et se comporte (si tout va bien) comme prévu. Comme vous avez supprimé la dépendance au système de base de données externe modifiable et à EF, vous pouvez alors vérifier de manière fiable les appels au référentiel dans vos tests unitaires.

Une grande partie des frameworks de mocking consiste à fournir la possibilité de vérifier les appels sur les objets Mock pour tester le comportement. Dans l'exemple ci-dessus, votre test ne teste en fait que la fonctionnalité d'ajout de DbSet, ce qui ne devrait pas vous préoccuper puisque MS a des tests unitaires pour cela. Ce que vous voulez savoir, c'est que l'appel à la fonction Add on DbSet a été effectué à partir de votre propre code de dépôt, le cas échéant, et c'est là que les frameworks Mock entrent en jeu.

Désolé, je sais que cela fait beaucoup de choses à digérer mais si vous lisez cet article, beaucoup de choses deviendront plus claires car Scott Allen est bien meilleur que moi pour expliquer ce genre de choses :)

41voto

Merritt Points 1684

À partir de ce que Daz a dit (et comme je l'ai mentionné dans ses commentaires), vous allez devoir créer une "fausse" implémentation d'IDbSet. Un exemple pour CTP 4 peut être trouvé aquí mais pour que cela fonctionne, vous devrez personnaliser votre méthode Find et ajouter des valeurs de retour pour certaines des méthodes précédemment annulées, comme Add.

Ce qui suit est un exemple que j'ai créé moi-même pour le 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; }
    }
}

0voto

Spock Points 3492

Vous obtenez 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 possède un constructeur protégé.

L'approche généralement adoptée pour favoriser la testabilité consiste à créer votre propre implémentation de l'ensemble de données.

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

Utilisez ceci comme

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

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

Tests unitaires avec EF4 "Code First" et Repository

Ensuite, le

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

Ne doit pas lever une exception.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X