Comment les personnes testent-elles les unités avec Entity Framework 6, devriez-vous vous embêter?

170

Je ne fais que commencer avec les tests unitaires et le TDD en général. J'ai déjà essayé mais maintenant je suis déterminé à l'ajouter à mon flux de travail et à écrire de meilleurs logiciels.

J'ai posé une question hier qui incluait ceci, mais cela semble être une question en soi. Je me suis assis pour commencer à implémenter une classe de service que j'utiliserai pour faire abstraction de la logique métier des contrôleurs et mapper vers des modèles spécifiques et des interactions de données à l'aide d'EF6.

Le problème est que je me suis déjà bloqué moi-même parce que je ne voulais pas extraire EF dans un référentiel (il sera toujours disponible en dehors des services pour des requêtes spécifiques, etc.) et je voudrais tester mes services (le contexte EF sera utilisé) .

Voici, je suppose, la question, est-il utile de faire cela? Si tel est le cas, comment les gens le font-ils dans la nature à la lumière des abstractions qui fuient causées par IQueryable et des nombreux articles intéressants de Ladislav Mrnka sur le sujet des tests unitaires ne sont pas simples en raison des différences entre les fournisseurs Linq lorsque vous travaillez avec un en mémoire mise en œuvre comme apposée à une base de données spécifique.

Le code que je veux tester semble assez simple. (ce n'est que du code factice pour essayer de comprendre ce que je fais, je veux piloter la création en utilisant TDD)

Le contexte

public interface IContext
{
    IDbSet<Product> Products { get; set; }
    IDbSet<Category> Categories { get; set; }
    int SaveChanges();
}

public class DataContext : DbContext, IContext
{
    public IDbSet<Product> Products { get; set; }
    public IDbSet<Category> Categories { get; set; }

    public DataContext(string connectionString)
                : base(connectionString)
    {

    }
}

Un service

public class ProductService : IProductService
{
    private IContext _context;

    public ProductService(IContext dbContext)
    {
        _context = dbContext;
    }

    public IEnumerable<Product> GetAll()
    {
        var query = from p in _context.Products
                    select p;

        return query;
    }
}

Actuellement, je suis dans l'esprit de faire plusieurs choses:

  1. Mocking EF Context avec quelque chose comme cette approche - Mocking EF When Unit Testing ou directement en utilisant un framework moqueur sur l'interface comme moq - en prenant la douleur que les tests unitaires peuvent réussir mais pas nécessairement de bout en bout et les sauvegarder avec des tests d'intégration?
  2. Peut-être utiliser quelque chose comme Effort pour se moquer d'EF - je ne l'ai jamais utilisé et je ne sais pas si quelqu'un d'autre l'utilise dans la nature?
  3. Pas la peine de tester quoi que ce soit qui rappelle simplement EF - donc essentiellement les méthodes de service qui appellent EF directement (getAll, etc.) ne sont pas testées à l'unité mais simplement testées par intégration?

Y a-t-il quelqu'un là-bas qui fait ça sans repo et qui a du succès?

Modika
la source
Hé Modika, j'y pensais récemment (à cause de cette question: stackoverflow.com/questions/25977388/… ) J'essaie de décrire un peu plus formellement comment je travaille en ce moment, mais j'aimerais savoir comment vous le faites.
samy
Salut @samy, la façon dont nous avons décidé de le faire n'était pas de tester un appareil qui touchait directement EF. Les requêtes ont été testées mais en tant que test d'intégration, pas de tests unitaires. Se moquer d'EF semble un peu sale, mais ce projet était petit, donc l'impact sur les performances d'avoir beaucoup de tests sur une base de données n'était pas vraiment un problème, nous pourrions donc être un peu plus pragmatiques à ce sujet. Je ne suis toujours pas sûr à 100% de la meilleure approche pour être complètement honnête avec vous, à un moment donné, vous allez frapper EF (et votre DB) et les tests unitaires ne me conviennent pas ici.
Modika

Réponses:

186

C'est un sujet qui m'intéresse beaucoup. De nombreux puristes disent qu'il ne faut pas tester des technologies telles que EF et NHibernate. Ils ont raison, ils sont déjà testés très rigoureusement et comme une réponse précédente l'a indiqué, il est souvent inutile de passer beaucoup de temps à tester ce que vous ne possédez pas.

Cependant, vous possédez la base de données en dessous! C'est là que cette approche, à mon avis, échoue, vous n'avez pas besoin de tester que EF / NH fait correctement son travail. Vous devez tester que vos mappages / implémentations fonctionnent avec votre base de données. À mon avis, c'est l'une des parties les plus importantes d'un système que vous puissiez tester.

Cependant, à strictement parler, nous sortons du domaine des tests unitaires pour nous diriger vers les tests d'intégration, mais les principes restent les mêmes.

La première chose que vous devez faire est de pouvoir simuler votre DAL afin que votre BLL puisse être testé indépendamment d'EF et de SQL. Ce sont vos tests unitaires. Ensuite, vous devez concevoir vos tests d'intégration pour prouver votre DAL, à mon avis, ils sont tout aussi importants.

Il y a plusieurs choses à considérer:

  1. Votre base de données doit être dans un état connu à chaque test. La plupart des systèmes utilisent une sauvegarde ou créent des scripts pour cela.
  2. Chaque test doit être répétable
  3. Chaque test doit être atomique

Il existe deux approches principales pour configurer votre base de données, la première consiste à exécuter un script de création de base de données UnitTest. Cela garantit que votre base de données de tests unitaires sera toujours dans le même état au début de chaque test (vous pouvez le réinitialiser ou exécuter chaque test dans une transaction pour vous en assurer).

Votre autre option est ce que je fais, exécuter des configurations spécifiques pour chaque test individuel. Je pense que c'est la meilleure approche pour deux raisons principales:

  • Votre base de données est plus simple, vous n'avez pas besoin d'un schéma complet pour chaque test
  • Chaque test est plus sûr, si vous modifiez une valeur dans votre script de création, cela n'invalide pas des dizaines d'autres tests.

Malheureusement, votre compromis ici est la vitesse. Il faut du temps pour exécuter tous ces tests, pour exécuter tous ces scripts de configuration / suppression.

Un dernier point, il peut être très difficile d'écrire une si grande quantité de SQL pour tester votre ORM. C'est là que j'adopte une approche très méchante (les puristes ici ne seront pas d'accord avec moi). J'utilise mon ORM pour créer mon test! Plutôt que d'avoir un script séparé pour chaque test DAL dans mon système, j'ai une phase de configuration de test qui crée les objets, les attache au contexte et les enregistre. Je lance ensuite mon test.

C'est loin d'être la solution idéale, mais en pratique, je trouve que c'est BEAUCOUP plus facile à gérer (surtout lorsque vous avez plusieurs milliers de tests), sinon vous créez un nombre énorme de scripts. Pratique plutôt que pureté.

Je vais sans aucun doute revenir sur cette réponse dans quelques années (mois / jours) et ne pas être d'accord avec moi-même car mes approches ont changé - mais c'est mon approche actuelle.

Pour essayer de résumer tout ce que j'ai dit ci-dessus, voici mon test d'intégration DB typique:

[Test]
public void LoadUser()
{
  this.RunTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    return user.UserID;
  }, id => // the ID of the entity we need to load
  {
     var user = LoadMyUser(id); // load the entity
     Assert.AreEqual("Mr", user.Title); // test your properties
     Assert.AreEqual("Joe", user.Firstname);
     Assert.AreEqual("Bloggs", user.Lastname);
  }
}

L'essentiel à noter ici est que les sessions des deux boucles sont totalement indépendantes. Dans votre implémentation de RunTest vous devez vous assurer que le contexte est validé et détruit et que vos données ne peuvent provenir de votre base de données que pour la deuxième partie.

Modifier le 13/10/2014

J'ai dit que je réviserais probablement ce modèle au cours des prochains mois. Bien que je maintienne largement l'approche que j'ai préconisée ci-dessus, j'ai légèrement mis à jour mon mécanisme de test. J'ai maintenant tendance à créer les entités dans TestSetup et TestTearDown.

[SetUp]
public void Setup()
{
  this.SetupTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    this.UserID =  user.UserID;
  });
}

[TearDown]
public void TearDown()
{
   this.TearDownDatabase();
}

Ensuite, testez chaque propriété individuellement

[Test]
public void TestTitle()
{
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Mr", user.Title);
}

[Test]
public void TestFirstname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Joe", user.Firstname);
}

[Test]
public void TestLastname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Bloggs", user.Lastname);
}

Il y a plusieurs raisons à cette approche:

  • Il n'y a pas d'appels de base de données supplémentaires (une configuration, un démontage)
  • Les tests sont beaucoup plus granulaires, chaque test vérifie une propriété
  • La logique Setup / TearDown est supprimée des méthodes de test elles-mêmes

Je pense que cela rend la classe de test plus simple et les tests plus granulaires (les assertions simples sont bonnes )

Modifier le 03/05/2015

Une autre révision de cette approche. Bien que les configurations au niveau de la classe soient très utiles pour les tests tels que le chargement des propriétés, elles le sont moins lorsque les différentes configurations sont requises. Dans ce cas, la création d'une nouvelle classe pour chaque cas est excessive.

Pour vous aider, j'ai maintenant tendance à avoir deux classes de base SetupPerTestet SingleSetup. Ces deux classes exposent le framework selon les besoins.

Dans le SingleSetupnous avons un mécanisme très similaire à celui décrit dans ma première modification. Un exemple serait

public TestProperties : SingleSetup
{
  public int UserID {get;set;}

  public override DoSetup(ISession session)
  {
    var user = new User("Joe", "Bloggs");
    session.Save(user);
    this.UserID = user.UserID;
  }

  [Test]
  public void TestLastname()
  {
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Bloggs", user.Lastname);
  }

  [Test]
  public void TestFirstname()
  {
       var user = LoadMyUser(this.UserID);
       Assert.AreEqual("Joe", user.Firstname);
  }
}

Cependant, les références qui garantissent que seules les entités correctes sont chargées peuvent utiliser une approche SetupPerTest

public TestProperties : SetupPerTest
{
   [Test]
   public void EnsureCorrectReferenceIsLoaded()
   {
      int friendID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriend();
         session.Save(user);
         friendID = user.Friends.Single().FriendID;
      } () =>
      {
         var user = GetUser();
         Assert.AreEqual(friendID, user.Friends.Single().FriendID);
      });
   }
   [Test]
   public void EnsureOnlyCorrectFriendsAreLoaded()
   {
      int userID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriends(2);
         var user2 = CreateUserWithFriends(5);
         session.Save(user);
         session.Save(user2);
         userID = user.UserID;
      } () =>
      {
         var user = GetUser(userID);
         Assert.AreEqual(2, user.Friends.Count());
      });
   }
}

En résumé, les deux approches fonctionnent en fonction de ce que vous essayez de tester.

Liath
la source
2
Voici une approche différente des tests d'intégration. TL; DR - Utilisez l'application elle-même pour configurer les données de test, annuler une transaction par test.
Gert Arnold
3
@Liath, excellente réponse. Vous avez confirmé mes soupçons concernant les tests d'EF. Ma question est la suivante; votre exemple est pour un cas très concret, ce qui est bien. Cependant, comme vous l'avez noté, vous devrez peut-être tester des centaines d'entités. Conformément au principe DRY (Ne vous répétez pas), comment mettez-vous votre solution à l'échelle, sans répéter le même modèle de code de base à chaque fois?
Jeffrey A. Gochin le
4
Je ne suis pas d'accord avec cela, car cela évite complètement le problème. Les tests unitaires consistent à tester la logique de la fonction. Dans l'exemple OP, la logique a une dépendance sur un magasin de données. Vous avez raison quand vous dites de ne pas tester EF, mais ce n'est pas le problème. Le problème consiste à tester votre code indépendamment de la banque de données. Tester votre cartographie est un sujet totalement différent imo. Afin de tester que la logique interagit correctement avec les données, vous devez être en mesure de contrôler le magasin.
Sinaesthetic
7
Personne n'est sur la question de savoir si vous devez tester Entity Framework par lui-même. Ce qui se passe, c'est que vous devez tester une méthode qui effectue certaines tâches et effectue également un appel EF à la base de données. L'objectif est de simuler EF afin que vous puissiez tester cette méthode sans avoir besoin d'une base de données sur votre serveur de génération.
The Muffin Man
4
J'aime vraiment le voyage. Merci d'avoir ajouté des modifications au fil du temps - c'est comme lire le contrôle de la source et comprendre comment votre pensée a évolué. J'apprécie vraiment la distinction fonctionnelle (avec EF) et unité (EF simulée) aussi.
Tom Leys
21

Commentaires sur l'expérience d'effort ici

Après de nombreuses lectures, j'ai utilisé Effort dans mes tests: pendant les tests, le Contexte est construit par une usine qui renvoie une version en mémoire, ce qui me permet de tester à chaque fois sur une ardoise vierge. En dehors des tests, la fabrique est résolue en une qui renvoie tout le contexte.

Cependant, j'ai le sentiment que les tests par rapport à une maquette complète de la base de données ont tendance à ralentir les tests; vous vous rendez compte que vous devez prendre soin de mettre en place tout un tas de dépendances afin de tester une partie du système. Vous avez également tendance à dériver vers l'organisation de tests qui peuvent ne pas être liés, simplement parce qu'il n'y a qu'un seul objet énorme qui gère tout. Si vous ne faites pas attention, vous pouvez vous retrouver à faire des tests d'intégration au lieu de tests unitaires

J'aurais préféré tester contre quelque chose de plus abstrait plutôt qu'un énorme DBContext, mais je n'ai pas pu trouver le point idéal entre des tests significatifs et des tests bare-bone. Remerciez-le à mon inexpérience.

Je trouve donc l'effort intéressant; si vous avez besoin de démarrer, c'est un bon outil pour démarrer rapidement et obtenir des résultats. Cependant, je pense que quelque chose d'un peu plus élégant et abstrait devrait être la prochaine étape et c'est ce que je vais étudier ensuite. Mettre cet article en favoris pour voir où il va ensuite :)

Modifier pour ajouter : l'effort prend un certain temps pour s'échauffer, vous regardez donc env. 5 secondes au démarrage du test. Cela peut être un problème pour vous si vous avez besoin que votre suite de tests soit très efficace.


Modifié pour clarification:

J'ai utilisé Effort pour tester une application de service Web. Chaque message M qui entre est acheminé vers une IHandlerOf<M>via Windsor. Castle.Windsor résout le IHandlerOf<M>qui résout les dépendances du composant. L'une de ces dépendances est le DataContextFactory, qui permet au gestionnaire de demander la fabrique

Dans mes tests, j'instancie directement le composant IHandlerOf, je moque tous les sous-composants du SUT et gère l'effort enveloppé DataContextFactorydans le gestionnaire.

Cela signifie que je ne fais pas de test unitaire au sens strict, car la base de données est touchée par mes tests. Cependant, comme je l'ai dit ci-dessus, cela m'a permis de démarrer et j'ai pu tester rapidement certains points de l'application

samy
la source
Merci pour votre contribution, ce que je peux faire car je dois faire fonctionner ce projet car il s'agit d'un travail rémunérateur de bonne foi est de commencer par des pensions et de voir comment je vais, mais l'effort est très intéressant. Par intérêt à quelle couche avez-vous utilisé l'effort dans vos applications?
Modika
2
seulement si Effort avait correctement pris en charge les transactions
Sedat Kapanoglu
et l'effort a un bogue pour les chaînes avec le chargeur csv, lorsque nous utilisons «» au lieu de null dans les chaînes.
Sam
13

Si vous souhaitez tester le code unitaire , vous devez isoler votre code que vous souhaitez tester (dans ce cas, votre service) des ressources externes (par exemple les bases de données). Vous pourriez probablement le faire avec une sorte de fournisseur EF en mémoire , mais une manière beaucoup plus courante consiste à abstraire votre implémentation EF, par exemple avec une sorte de modèle de référentiel. Sans cet isolement, tous les tests que vous écrivez seront des tests d'intégration, pas des tests unitaires.

En ce qui concerne le test du code EF - j'écris des tests d'intégration automatisés pour mes référentiels qui écrivent diverses lignes dans la base de données lors de leur initialisation, puis j'appelle mes implémentations de référentiel pour m'assurer qu'elles se comportent comme prévu (par exemple, en m'assurant que les résultats sont correctement filtrés, ou qu'ils sont triés dans le bon ordre).

Il s'agit de tests d'intégration et non de tests unitaires, car les tests reposent sur la présence d'une connexion à la base de données et sur le fait que la base de données cible dispose déjà du dernier schéma à jour installé.

Justin
la source
Merci @justin, je connais le modèle Repository, mais la lecture de choses comme ayende.com/blog/4784/… et lostechies.com/jimmybogard/2009/09/11/wither-the-repository entre autres m'a fait penser que je ne Je ne veux pas de cette couche d'abstraction, mais là encore, ceux-ci parlent davantage d'une approche de requête qui devient très déroutante.
Modika
7
@Modika Ayende a choisi une mauvaise implémentation du modèle de référentiel à critiquer, et par conséquent, elle a raison à 100% - elle est sur-conçue et n'offre aucun avantage. Une bonne implémentation isole les parties testables unitaires de votre code de l'implémentation DAL. L'utilisation de NHibernate et EF rend le code difficile (voire impossible) à tester unitaire et conduit à une base de code monolithique rigide. Je suis encore un peu sceptique quant au modèle de référentiel, mais je suis convaincu à 100% que vous devez isoler votre implémentation DAL d'une manière ou d'une autre et le référentiel est la meilleure chose que j'ai trouvée jusqu'à présent.
Justin
2
@Modika Relisez le deuxième article. «Je ne veux pas de cette couche d'abstraction» n'est pas ce qu'il dit. De plus, lisez sur le modèle de référentiel original de Fowler ( martinfowler.com/eaaCatalog/repository.html ) ou DDD ( dddcommunity.org/resources/ddd_terms ). Ne croyez pas les opposants sans comprendre pleinement le concept original. Ce qu'ils critiquent vraiment, c'est une utilisation abusive récente du modèle, pas du modèle lui-même (bien qu'ils ne le sachent probablement pas).
guillaume31
1
@ guillaume31 je ne suis pas contre le modèle de référentiel (je le comprends) j'essaie simplement de savoir si j'en ai besoin pour faire abstraction de ce qui est déjà une abstraction à ce niveau, et si je peux l'omettre et tester directement contre EF en se moquant et utilisez-le dans mes tests à un niveau supérieur dans mon application. De plus, si je n'utilise pas de dépôt, je profite de l'ensemble de fonctionnalités étendu EF, avec un dépôt, je ne peux pas l'obtenir.
Modika
Une fois que j'ai isolé le DAL avec un référentiel, j'ai besoin d'une manière ou d'une autre de "Mock" la base de données (EF). Jusqu'à présent, se moquer du contexte et de diverses extensions asynchrones (ToListAsync (), FirstOrDefaultAsync (), etc.) ont entraîné de la frustration pour moi.
Kevin Burton
9

Alors, voici le problème, Entity Framework est une implémentation, donc malgré le fait qu'il résume la complexité de l'interaction avec la base de données, interagir directement est toujours un couplage étroit et c'est pourquoi il est difficile de tester.

Les tests unitaires consistent à tester la logique d'une fonction et chacun de ses résultats potentiels indépendamment de toute dépendance externe, qui dans ce cas est le magasin de données. Pour ce faire, vous devez être en mesure de contrôler le comportement du magasin de données. Par exemple, si vous voulez affirmer que votre fonction retourne false si l'utilisateur récupéré ne répond pas à un ensemble de critères, alors votre magasin de données [simulé] doit être configuré pour toujours renvoyer un utilisateur qui ne répond pas aux critères, et vice-versa versa pour l'affirmation opposée.

Cela dit, et en acceptant le fait qu'EF est une implémentation, je préférerais probablement l'idée d'abstraire un référentiel. Vous semblez un peu redondant? Ce n'est pas le cas, car vous résolvez un problème qui isole votre code de l'implémentation des données.

Dans DDD, les référentiels ne renvoient que des racines agrégées, pas DAO. De cette façon, le consommateur du référentiel n'a jamais à connaître l'implémentation des données (comme il ne devrait pas) et nous pouvons l'utiliser comme exemple pour résoudre ce problème. Dans ce cas, l'objet généré par EF est un DAO et, en tant que tel, doit être masqué de votre application. C'est un autre avantage du référentiel que vous définissez. Vous pouvez définir un objet métier comme son type de retour au lieu de l'objet EF. Désormais, le dépôt masque les appels à EF et mappe la réponse EF à cet objet métier défini dans la signature du dépôt. Vous pouvez maintenant utiliser ce dépôt à la place de la dépendance DbContext que vous injectez dans vos classes et par conséquent, vous pouvez maintenant vous moquer de cette interface pour vous donner le contrôle dont vous avez besoin pour tester votre code de manière isolée.

C'est un peu plus de travail et beaucoup de pieds de nez, mais cela résout un vrai problème. Il y a un fournisseur en mémoire qui a été mentionné dans une réponse différente qui pourrait être une option (je ne l'ai pas essayé), et son existence même est la preuve de la nécessité de la pratique.

Je suis complètement en désaccord avec la réponse principale car elle évite le vrai problème qui consiste à isoler votre code, puis passe à une tangente pour tester votre mappage. Bien sûr, testez votre mappage si vous le souhaitez, mais résolvez le problème réel ici et obtenez une couverture de code réelle.

Sinaesthésique
la source
8

Je ne voudrais pas de code de test unitaire que je ne possède pas. Que testez-vous ici, que le compilateur MSFT fonctionne?

Cela dit, pour rendre ce code testable, vous DEVEZ presque séparer votre couche d'accès aux données de votre code de logique métier. Ce que je fais est de prendre tous mes trucs EF et de les mettre dans une (ou plusieurs) classe DAO ou DAL qui a également une interface correspondante. Ensuite, j'écris mon service qui aura l'objet DAO ou DAL injecté en tant que dépendance (injection de constructeur de préférence) référencée comme interface. Maintenant, la partie qui doit être testée (votre code) peut facilement être testée en simulant l'interface DAO et en l'injectant dans votre instance de service à l'intérieur de votre test unitaire.

//this is testable just inject a mock of IProductDAO during unit testing
public class ProductService : IProductService
{
    private IProductDAO _productDAO;

    public ProductService(IProductDAO productDAO)
    {
        _productDAO = productDAO;
    }

    public List<Product> GetAllProducts()
    {
        return _productDAO.GetAll();
    }

    ...
}

Je considère que les couches d'accès aux données en direct font partie des tests d'intégration, pas des tests unitaires. J'ai vu des gars effectuer des vérifications sur le nombre de voyages vers la base de données hibernate avant, mais ils étaient sur un projet qui impliquait des milliards d'enregistrements dans leur magasin de données et ces voyages supplémentaires importaient vraiment.

Jonathan Henson
la source
1
Merci pour la réponse, mais quelle serait la différence de dire un référentiel où vous cachez les composants internes d'EF derrière lui à ce niveau? Je ne veux pas vraiment faire abstraction d'EF, bien que je puisse toujours le faire avec l'interface IContext? Je suis nouveau dans ce domaine, soyez doux :)
Modika
3
@Modika Un Repo est bien aussi. Quel que soit le modèle que vous voulez. "Je ne veux pas vraiment abstraire EF" Voulez-vous du code testable ou non?
Jonathan Henson
1
@Modika mon point est que vous n'aurez AUCUN code testable si vous ne séparez pas vos préoccupations. L'accès aux données et la logique métier DOIVENT être dans des couches séparées pour réaliser de bons tests maintenables.
Jonathan Henson
2
Je n'ai tout simplement pas jugé nécessaire d'envelopper EF dans une abstraction de référentiel car essentiellement les IDbSets sont des dépôts et le contexte de l'UOW, je mettrai à jour un peu ma question car cela peut être trompeur. Le problème vient avec n'importe quelle abstraction et le point principal est ce que je teste exactement parce que mes demandes ne fonctionneront pas dans les mêmes limites (linq-vers-entités vs linq-vers-objets) donc si je teste simplement que mon service fait un appel qui semble un peu inutile ou suis-je bien ici?
Modika
1
, Bien que je sois d'accord avec vos points généraux, DbContext est une unité de travail et les IDbSets sont certainement des exemples d'implémentation de référentiel, et je ne suis pas le seul à le penser. Je peux me moquer d'EF, et à une certaine couche, je devrai exécuter des tests d'intégration, est-ce vraiment important si je le fais dans un référentiel ou plus haut dans un service? Être étroitement couplé à une base de données n'est pas vraiment un problème, je suis sûr que cela arrive mais je ne vais pas planifier quelque chose qui pourrait ne pas se produire.
Modika
8

J'ai parfois cherché à atteindre ces considérations:

1- Si mon application accède à la base de données, pourquoi le test ne devrait pas? Et s'il y a un problème avec l'accès aux données? Les tests doivent le savoir au préalable et m'alerter du problème.

2- Le modèle de référentiel est un peu difficile et prend du temps.

J'ai donc proposé cette approche, que je ne pense pas être la meilleure, mais qui a répondu à mes attentes:

Use TransactionScope in the tests methods to avoid changes in the database.

Pour ce faire, il faut:

1- Installez EntityFramework dans le projet de test. 2- Mettez la chaîne de connexion dans le fichier app.config de Test Project. 3- Référencez la dll System.Transactions dans Test Project.

L'effet secondaire unique est que la graine d'identité s'incrémentera lors de la tentative d'insertion, même lorsque la transaction est abandonnée. Mais comme les tests sont effectués sur une base de données de développement, cela ne devrait pas poser de problème.

Exemple de code:

[TestClass]
public class NameValueTest
{
    [TestMethod]
    public void Edit()
    {
        NameValueController controller = new NameValueController();

        using(var ts = new TransactionScope()) {
            Assert.IsNotNull(controller.Edit(new Models.NameValue()
            {
                NameValueId = 1,
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }

    [TestMethod]
    public void Create()
    {
        NameValueController controller = new NameValueController();

        using (var ts = new TransactionScope())
        {
            Assert.IsNotNull(controller.Create(new Models.NameValue()
            {
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }
}
Marquinho Peli
la source
1
En fait, j'aime beaucoup cette solution. Scénarios de test très simples à mettre en œuvre et plus réalistes. Merci!
slopapa
1
avec EF 6, vous utiliseriez DbContext.Database.BeginTransaction, n'est-ce pas?
SwissCoder le
5

En bref, je dirais non, le jus ne vaut pas la peine de tester une méthode de service avec une seule ligne qui récupère les données du modèle. D'après mon expérience, les nouveaux utilisateurs de TDD veulent absolument tout tester. Le vieux châtain d'abstraire une façade à un framework tiers juste pour que vous puissiez créer une maquette de cette API de framework avec laquelle vous bastardisez / étendez afin que vous puissiez injecter des données factices a peu de valeur dans mon esprit. Tout le monde a une vision différente de la qualité des tests unitaires. J'ai tendance à être plus pragmatique ces jours-ci et à me demander si mon test ajoute vraiment de la valeur au produit final, et à quel prix.

Entrez
la source
1
Oui au pragmatisme. Je soutiens toujours que la qualité de vos tests unitaires est inférieure à la qualité de votre code d'origine. Bien sûr, il est utile d'utiliser TDD pour améliorer votre pratique de codage et également pour améliorer la maintenabilité, mais TDD peut avoir une valeur décroissante. Nous exécutons tous nos tests sur la base de données, car cela nous donne la certitude que notre utilisation d'EF et des tables elles-mêmes est correcte. Les tests prennent plus de temps à s'exécuter, mais ils sont plus fiables.
Savage
3

Je souhaite partager une approche commentée et brièvement discutée, mais montrer un exemple réel que j'utilise actuellement pour aider à tester les services basés sur EF.

Tout d'abord, j'aimerais utiliser le fournisseur en mémoire d'EF Core, mais il s'agit d'EF 6. De plus, pour d'autres systèmes de stockage comme RavenDB, je serais également partisan des tests via le fournisseur de base de données en mémoire. Encore une fois, il s'agit spécifiquement d'aider à tester le code basé sur EF sans trop de cérémonie .

Voici les objectifs que j'avais en créant un modèle:

  • Il doit être simple pour les autres développeurs de l'équipe de comprendre
  • Il doit isoler le code EF au niveau le plus bas possible
  • Cela ne doit pas impliquer la création d'interfaces multi-responsabilités étranges (comme un modèle de référentiel «générique» ou «typique»)
  • Il doit être facile à configurer et à installer dans un test unitaire

Je suis d'accord avec les déclarations précédentes selon lesquelles EF est toujours un détail d'implémentation et il est normal de penser que vous devez l'abstraction afin de faire un test unitaire "pur". Je conviens également que, idéalement, je voudrais m'assurer que le code EF lui-même fonctionne - mais cela implique une base de données sandbox, un fournisseur en mémoire, etc. Mon approche résout les deux problèmes - vous pouvez tester en toute sécurité le code dépendant d'EF et créer tests d'intégration pour tester votre code EF spécifiquement.

Pour ce faire, j'ai simplement encapsulé du code EF dans des classes de requête et de commande dédiées. L'idée est simple: enveloppez simplement n'importe quel code EF dans une classe et dépendez d'une interface dans les classes qui l'auraient utilisé à l'origine. Le principal problème que je devais résoudre était d'éviter d'ajouter de nombreuses dépendances aux classes et de configurer beaucoup de code dans mes tests.

C'est là qu'une bibliothèque simple et utile entre en jeu: Mediatr . Il permet une messagerie simple en cours de processus et le fait en découplant les «requêtes» des gestionnaires qui implémentent le code. Cela présente un avantage supplémentaire de découpler le «quoi» du «comment». Par exemple, en encapsulant le code EF en petits morceaux, cela vous permet de remplacer les implémentations par un autre fournisseur ou un mécanisme totalement différent, car tout ce que vous faites est d'envoyer une demande pour effectuer une action.

En utilisant l'injection de dépendances (avec ou sans framework - votre préférence), nous pouvons facilement nous moquer du médiateur et contrôler les mécanismes de demande / réponse pour activer le code EF de test unitaire.

Tout d'abord, disons que nous avons un service qui a une logique métier que nous devons tester:

public class FeatureService {

  private readonly IMediator _mediator;

  public FeatureService(IMediator mediator) {
    _mediator = mediator;
  }

  public async Task ComplexBusinessLogic() {
    // retrieve relevant objects

    var results = await _mediator.Send(new GetRelevantDbObjectsQuery());
    // normally, this would have looked like...
    // var results = _myDbContext.DbObjects.Where(x => foo).ToList();

    // perform business logic
    // ...    
  }
}

Commencez-vous à voir les avantages de cette approche? Non seulement vous encapsulez explicitement tout le code lié à EF dans des classes descriptives, mais vous autorisez l'extensibilité en supprimant le problème d'implémentation de "comment" cette demande est gérée - cette classe ne se soucie pas si les objets pertinents proviennent d'EF, MongoDB, ou un fichier texte.

Maintenant pour la requête et le gestionnaire, via MediatR:

public class GetRelevantDbObjectsQuery : IRequest<DbObject[]> {
  // no input needed for this particular request,
  // but you would simply add plain properties here if needed
}

public class GetRelevantDbObjectsEFQueryHandler : IRequestHandler<GetRelevantDbObjectsQuery, DbObject[]> {
  private readonly IDbContext _db;

  public GetRelevantDbObjectsEFQueryHandler(IDbContext db) {
    _db = db;
  }

  public DbObject[] Handle(GetRelevantDbObjectsQuery message) {
    return _db.DbObjects.Where(foo => bar).ToList();
  }
}

Comme vous pouvez le voir, l'abstraction est simple et encapsulée. Il est également absolument testable car dans un test d'intégration, vous pouvez tester cette classe individuellement - il n'y a pas de problèmes commerciaux mélangés ici.

Alors, à quoi ressemble un test unitaire de notre service d'entités? C'est très simple. Dans ce cas, j'utilise Moq pour me moquer (utilisez ce qui vous rend heureux):

[TestClass]
public class FeatureServiceTests {

  // mock of Mediator to handle request/responses
  private Mock<IMediator> _mediator;

  // subject under test
  private FeatureService _sut;

  [TestInitialize]
  public void Setup() {

    // set up Mediator mock
    _mediator = new Mock<IMediator>(MockBehavior.Strict);

    // inject mock as dependency
    _sut = new FeatureService(_mediator.Object);
  }

  [TestCleanup]
  public void Teardown() {

    // ensure we have called or expected all calls to Mediator
    _mediator.VerifyAll();
  }

  [TestMethod]
  public void ComplexBusinessLogic_Does_What_I_Expect() {
    var dbObjects = new List<DbObject>() {
      // set up any test objects
      new DbObject() { }
    };

    // arrange

    // setup Mediator to return our fake objects when it receives a message to perform our query
    // in practice, I find it better to create an extension method that encapsulates this setup here
    _mediator.Setup(x => x.Send(It.IsAny<GetRelevantDbObjectsQuery>(), default(CancellationToken)).ReturnsAsync(dbObjects.ToArray()).Callback(
    (GetRelevantDbObjectsQuery message, CancellationToken token) => {
       // using Moq Callback functionality, you can make assertions
       // on expected request being passed in
       Assert.IsNotNull(message);
    });

    // act
    _sut.ComplexBusinessLogic();

    // assertions
  }

}

Vous pouvez voir que tout ce dont nous avons besoin est une configuration unique et nous n'avons même pas besoin de configurer quoi que ce soit de plus - c'est un test unitaire très simple. Soyons clairs: Cela est tout à fait possible de le faire sans quelque chose comme Mediatr (vous simplement implémenter une interface et raillent pour des tests, par exemple IGetRelevantDbObjectsQuery), mais dans la pratique pour une grande base de code avec de nombreuses fonctionnalités et requêtes / commandes, j'aime l'encapsulation et offre de support DI innée Mediatr.

Si vous vous demandez comment j'organise ces cours, c'est assez simple:

- MyProject
  - Features
    - MyFeature
      - Queries
      - Commands
      - Services
      - DependencyConfig.cs (Ninject feature modules)

L'organisation par tranches de fonctionnalités n'est pas la question, mais cela permet de garder ensemble tout le code pertinent / dépendant et facilement découvrable. Plus important encore, je sépare les requêtes des commandes - en suivant le principe de séparation commande / requête .

Cela répond à tous mes critères: c'est une cérémonie simple, c'est facile à comprendre et il y a des avantages cachés supplémentaires. Par exemple, comment gérez-vous l'enregistrement des modifications? Vous pouvez désormais simplifier votre contexte Db en utilisant une interface de rôle (IUnitOfWork.SaveChangesAsync()) et simulez des appels à l'interface de rôle unique ou vous pouvez encapsuler la validation / la restauration dans vos RequestHandlers - mais vous préférez le faire dépend de vous, tant que cela peut être maintenu. Par exemple, j'ai été tenté de créer une seule requête / gestionnaire générique où vous passeriez simplement un objet EF et il le sauverait / mettrait à jour / le supprimerait - mais vous devez demander quelle est votre intention et vous en souvenir si vous le souhaitez échangez le gestionnaire avec un autre fournisseur / implémentation de stockage, vous devriez probablement créer des commandes / requêtes explicites qui représentent ce que vous avez l'intention de faire. Le plus souvent, un service ou une fonctionnalité unique aura besoin de quelque chose de spécifique - ne créez pas d'éléments génériques avant d'en avoir besoin.

Il y a bien sûr des réserves à ce modèle - vous pouvez aller trop loin avec un simple mécanisme pub / sub. J'ai limité mon implémentation à l'abstraction du code lié à EF, mais les développeurs aventureux pourraient commencer à utiliser MediatR pour aller trop loin et tout envoyer par message - quelque chose de bonnes pratiques de révision de code et les évaluations par les pairs devraient attraper. C'est un problème de processus, pas un problème avec MediatR, alors soyez conscient de la façon dont vous utilisez ce modèle.

Vous vouliez un exemple concret de la façon dont les gens testent / se moquent d'EF et c'est une approche qui fonctionne avec succès pour nous sur notre projet - et l'équipe est très satisfaite de la facilité avec laquelle il est à adopter. J'espère que ça aide! Comme pour tout ce qui concerne la programmation, il existe plusieurs approches et tout dépend de ce que vous voulez réaliser. J'apprécie la simplicité, la facilité d'utilisation, la maintenabilité et la découvrabilité - et cette solution répond à toutes ces exigences.

Kamranicus
la source
Merci pour la réponse, c'est une excellente description du modèle QueryObject à l'aide d'un médiateur, et quelque chose que je commence également à introduire dans mes projets. Je devrai peut-être mettre à jour la question, mais je ne teste plus unités EF, les abstractions sont trop fuites (SqlLite peut être correct cependant) donc je teste simplement mes choses qui interrogent la base de données et les règles métier des tests unitaires et d'autres logiques.
Modika
3

Il y a Effort qui est un fournisseur de base de données de structure d'entité en mémoire. Je n'ai pas vraiment essayé ... Haa vient de voir que cela a été mentionné dans la question!

Vous pouvez également basculer vers EntityFrameworkCore qui a un fournisseur de base de données en mémoire intégré.

https://blog.goyello.com/2016/07/14/save-time-mocking-use-your-real-entity-framework-dbcontext-in-unit-tests/

https://github.com/tamasflamich/effort

J'ai utilisé une fabrique pour obtenir un contexte, donc je peux créer le contexte proche de son utilisation. Cela semble fonctionner localement dans Visual Studio mais pas sur mon serveur de build TeamCity, je ne sais pas encore pourquoi.

return new MyContext(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");
andrew pate
la source
Salut Andrew, le problème n'a jamais été d'obtenir le contexte, vous pouvez fabriquer le contexte qui est ce que nous faisions, en faisant abstraction du contexte et en le faisant construire par l'usine. Le plus gros problème était la cohérence de ce qui était en mémoire par rapport à ce que fait Linq4Entities, ils ne sont pas les mêmes, ce qui peut conduire à des tests trompeurs. Actuellement, nous ne faisons que tester l'intégration de la base de données, ce n'est peut-être pas le meilleur processus pour tout le monde.
Modika
Cet assistant Moq fonctionne ( codeproject.com/Tips/1045590/… ) si vous avez un contexte à simuler. Si vous sauvegardez le contexte simulé avec une liste, il ne se comportera pas comme un contexte sauvegardé par une base de données SQL.
andrew pate
2

J'aime séparer mes filtres des autres parties du code et les tester comme je le décris sur mon blog ici http://coding.grax.com/2013/08/testing-custom-linq-filter-operators.html

Cela étant dit, la logique de filtrage testée n'est pas identique à la logique de filtrage exécutée lorsque le programme est exécuté en raison de la traduction entre l'expression LINQ et le langage de requête sous-jacent, tel que T-SQL. Pourtant, cela me permet de valider la logique du filtre. Je ne m'inquiète pas trop des traductions qui se produisent et des choses telles que la sensibilité à la casse et la gestion des valeurs nulles jusqu'à ce que je teste l'intégration entre les couches.

Grax32
la source
0

Il est important de tester ce que vous attendez du cadre d'entité (c.-à-d. Valider vos attentes). Une façon de faire cela que j'ai utilisée avec succès consiste à utiliser moq comme indiqué dans cet exemple (trop long pour copier dans cette réponse):

https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking

Cependant, soyez prudent ... Un contexte SQL n'est pas garanti de renvoyer les choses dans un ordre spécifique à moins que vous n'ayez un "OrderBy" approprié dans votre requête linq, il est donc possible d'écrire des choses qui réussissent lorsque vous testez en utilisant une liste en mémoire ( linq-to-entity) mais échoue dans votre environnement uat / live lorsque (linq-to-sql) est utilisé.

andrew pate
la source