web-dev-qa-db-fra.com

Comment les personnes effectuent-elles des tests unitaires avec Entity Framework 6?

Je commence tout juste avec les tests unitaires et le TDD en général. J'ai déjà essayé un peu mais maintenant je suis déterminé à l'ajouter à mon flux de travail et à écrire de meilleurs logiciels.

Hier, j'ai posé une question qui incluait cela, mais il semble que ce soit une question en soi. Je me suis assis pour commencer à mettre en œuvre une classe de service que je vais utiliser pour extraire la logique métier des contrôleurs et mapper vers des modèles spécifiques et des interactions de données à l'aide de EF6.

Le problème est que je me suis déjà bloqué parce que je ne voulais pas résumer 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é). .

Ici, je suppose que la question est la suivante: y a-t-il lieu de faire cela? Si oui, comment les gens le font-ils dans la nature à la lumière des abstractions qui fuient causées par IQueryable et des nombreux grands messages de Ladislav Mrnka sur le sujet des tests unitaires qui ne sont pas simples en raison des différences entre Linq fournisseurs lorsqu’ils travaillent avec une implémentation en mémoire associée à une base de données spécifique.

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

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)
    {

    }
}

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 quelques choses:

  1. Mocking EF Contexte avec quelque chose comme cette approche- Mocking EF lors des tests unitaires ou directement à l'aide d'un framework moqueur sur l'interface comme moq - prendre la peine que les tests unitaires peuvent réussir mais ne fonctionnent 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 de EF - je ne l'ai jamais utilisé et je ne suis pas sûr que quelqu'un l'utilise à l'état sauvage?
  3. Ne vous souciez pas de tester tout ce qui rappelle simplement à EF - les méthodes de service qui appellent directement EF (getAll, etc.) ne sont donc pas testées à l'unité, mais uniquement testées en intégration?

Quelqu'un fait-il cela sans y avoir de pension et avoir du succès?

162
Modika

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 ont déjà été soumis à des tests très stricts et, comme indiqué dans une réponse précédente, 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 ci-dessous! C’est là que cette approche, à mon avis, est défaillante, vous n’avez pas besoin de tester que EF/NH est faire leur travail correctement. Vous devez vérifier 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 pouvez tester.

Strictement parlant, nous sortons toutefois du domaine des tests unitaires pour des tests d’intégration, mais les principes restent les mêmes.

La première chose à faire est de pouvoir vous moquer de votre DAL afin que votre BLL puisse être testée indépendamment de 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 soit le réinitialiser, soit 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 crois 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 est la rapidité. Il faut du temps pour exécuter tous ces tests, pour exécuter tous ces scripts d'installation/de démontage.

Un dernier point, il peut être très difficile d’écrire une telle quantité de SQL pour tester votre ORM. C’est là que j’adopte une approche très méchante (les puristes d’ici ne seront pas d’accord avec moi). J'utilise mon ORM pour créer mon test! Plutôt que d'avoir un script distinct pour chaque test DAL de 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 dans la pratique, j’estime que c’est beaucoup plus facile à gérer (surtout lorsque vous avez plusieurs milliers de tests), sinon vous créez un grand nombre de scripts. La praticité avant la pureté.

Je reviendrai sans doute sur cette réponse dans quelques années (mois/jours) et je ne serai pas 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 plus haut, voici mon test d'intégration de base de données 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);
  }
}

La chose clé à noter ici est que les sessions des deux boucles sont complètement 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.

Éditer 13/10/2014

J'ai dit que je réviserais probablement ce modèle au cours des prochains mois. Bien que je maintienne en grande partie l'approche que je préconisais, 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();
}

Puis 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 simplifie la classe de test et rend les tests plus granulaires ( les assertions simples sont bonnes )

Edit 5/3/2015

Une autre révision de cette approche. Bien que les configurations au niveau 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, configurer une nouvelle classe pour chaque cas est excessif.

Pour aider avec ceci, j'ai maintenant tendance à avoir deux classes de base SetupPerTest et SingleSetup. Ces deux classes exposent le cadre si nécessaire.

Dans le SingleSetup nous avons un mécanisme très similaire à celui décrit dans ma première édition. 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 garantissant 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.

173
Liath

Effort Experience Feedback ici

Après de nombreuses lectures, j’utilise Effort dans mes tests: pendant les tests, le contexte est créé par une fabrique qui renvoie une version en mémoire, ce qui me permet de tester chaque fois une page vierge. En dehors des tests, l’usine est résolue en une unité qui renvoie le contexte entier.

Cependant, j’ai le sentiment que les tests effectués contre une maquette complète de la base de données ont tendance à faire glisser les tests vers le bas; vous réalisez que vous devez prendre en charge la configuration de nombreuses dépendances afin de tester une partie du système. Vous avez également tendance à vouloir organiser ensemble des tests qui ne sont peut-être pas liés, simplement parce qu'il n'y a qu'un seul objet énorme qui gère tout. Si vous n'y prêtez pas attention, vous risquez peut-être de faire des tests d'intégration plutôt que des tests unitaires.

J'aurais préféré tester contre quelque chose de plus abstrait plutôt qu'un énorme DBContext, mais je ne trouvais pas le compromis idéal entre des tests significatifs et des tests à nu. Craignez-le à mon inexpérience.

Je trouve donc l’effort intéressant; Si vous devez vous mettre au travail, 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. Favoriser ce post pour voir où il va ensuite :)

Modifier pour ajouter : Il faut un certain temps avant que l’effort se réchauffe, de sorte que vous regardez environ. 5 secondes au démarrage du test. Cela peut être un problème pour vous si votre suite de tests doit être très efficace.


Édité pour clarification:

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

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

Cela signifie que je ne fais pas de tests unitaires au sens strict, car la base de données est touchée par mes tests. Cependant, comme je l’ai dit plus haut, cela m’a permis de lancer le programme rapidement et de tester rapidement certains points de l’application.

20
samy

Si vous voulez unité tester le code, vous devez alors isoler le code que vous souhaitez tester (dans le cas présent, votre service) des ressources externes (par exemple, des bases de données). . Vous pouvez probablement le faire avec une sorte de fournisseur EF en mémoire , mais un moyen beaucoup plus courant consiste à faire abstraction de votre implémentation EF, par exemple. avec une sorte de modèle de référentiel. Sans cette isolation, les tests que vous écrivez seront des tests d'intégration et non des tests unitaires.

En ce qui concerne les tests de 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 au cours de leur initialisation, puis appelle les implémentations de mon référentiel pour vérifier qu’elles se comportent comme prévu (par exemple, en veillant à ce que les résultats soient filtrés correctement). qu'ils sont triés dans le bon ordre).

Il s’agit de tests d’intégration et non de tests unitaires, car ils reposent sur une connexion à une base de données et que le dernier schéma mis à jour est déjà installé sur la base de données cible.

12
Justin

Voilà donc le problème: Entity Framework est une implémentation. Malgré le fait qu’il éloigne la complexité de l’interaction des bases de données, l’interaction directe est toujours un couplage étroit et c’est pourquoi il est difficile de tester.

Le test unitaire consiste à tester la logique d'une fonction et chacun de ses résultats potentiels indépendamment des dépendances externes, qui dans ce cas constituent le magasin de données. Pour ce faire, vous devez pouvoir contrôler le comportement du magasin de données. Par exemple, si vous souhaitez affirmer que votre fonction renvoie false si l'utilisateur récupéré ne répond pas à un ensemble de critères, votre magasin de données [fictif] doit être configuré pour toujours renvoyer un utilisateur qui ne répond pas aux critères, et vice-versa. vice versa pour l'affirmation opposée.

Cela dit, et en acceptant le fait que EF est une implémentation, je serais probablement en faveur de l’abstraction d’un référentiel. Vous semblez un peu redondant? Ce n'est pas parce que vous résolvez un problème qui isole votre code de l'implémentation de données.

Dans DDD, les référentiels ne renvoient jamais que les racines agrégées, pas les DAO. De cette manière, le consommateur du référentiel n’a jamais besoin de connaître l’implémentation des données (comme il ne devrait pas le faire) et nous pouvons l’utiliser comme exemple de la façon de 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. Ceci est un autre avantage du référentiel que vous définissez. Vous pouvez définir un objet métier comme type de retour au lieu de l'objet EF. À présent, le repo masque les appels à EF et mappe la réponse EF à cet objet métier défini dans la signature de mise en pension. Vous pouvez maintenant utiliser ce référentiel à la place de la dépendance DbContext que vous injectez dans vos classes. 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 s'en moquent, mais cela résout un problème réel. Il existe un fournisseur en mémoire mentionné dans une réponse différente qui pourrait être une option (je ne l'ai pas essayée), et son existence même est une preuve du besoin de la pratique.

Je suis complètement en désaccord avec la première réponse, car elle contourne le véritable problème qui consiste à isoler votre code, puis passe à la tangente en ce qui concerne le test de votre mappage. Bien sûr, testez votre correspondance si vous le souhaitez, mais abordez le problème ici et obtenez une couverture réelle du code.

8
Sinaesthetic

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

Cela dit, pour que ce code puisse être testé, 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 tout mon contenu EF et de le mettre dans une (ou plusieurs) classe DAO ou DAL qui possède également une interface correspondante. Ensuite, j'écris mon service dans lequel l'objet DAO ou DAL sera injecté en tant que dépendance (injection de constructeur de préférence) référencée en tant qu'interface. Désormais, la pièce à tester (votre code) peut facilement être testée en configurant l'interface DAO et en l'injectant dans votre instance de service dans le 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érerais que les couches d'accès aux données réelles font partie des tests d'intégration et non des tests unitaires. J'ai déjà vu des gars vérifier le nombre de visites effectuées dans la base de données d'Hibernate auparavant, mais elles participaient à un projet impliquant des milliards d'enregistrements dans leur magasin de données et ces sorties supplémentaires importaient vraiment.

7
Jonathan Henson

J'ai tâtonné quelque temps pour atteindre ces considérations:

1- Si mon application accède à la base de données, pourquoi le test ne devrait pas? Que se passe-t-il si quelque chose ne va pas avec l'accès aux données? Les tests doivent le savoir à l'avance et m'avertir 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 qui, à mon avis, n'est pas la meilleure, mais a répondu à mes attentes:

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

Pour le faire il faut:

1- Installez EntityFramework dans le projet test. 2- Placez 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é incrémentera lors d'une tentative d'insertion, même si la transaction est annulée. Mais comme les tests sont effectués sur une base de données de développement, cela ne devrait poser aucun 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();
        }
    }
}
7
Marquinho Peli

En bref, je dirais non, le jus ne vaut pas la peine de tester une méthode de service avec une seule ligne permettant de récupérer les données du modèle. D'après mon expérience, les personnes qui débutent chez TDD veulent tout tester. L'ancienne méthode consistant à extraire une façade d'un cadre tiers pour pouvoir créer une maquette de cette API avec laquelle vous bâtissez/étendez de manière à pouvoir injecter des données factices n'a guère de valeur pour moi. Tout le monde a un point de vue différent sur le meilleur nombre de tests unitaires. J'ai tendance à être plus pragmatique ces temps-ci et à me demander si mon test ajoute vraiment de la valeur au produit final et à quel coût.

5
ComeIn

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

Vous pouvez également basculer sur EntityFrameworkCore, qui possède 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-un-tests/

https://github.com/tamasflamich/effort

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

return new MyContext(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");
3
andrew pate

Je souhaite partager une approche commentée et brièvement discutée, mais montre un exemple concret que je suis en train d'utiliser pour aider le test unitaire des services basés sur EF.

Premièrement, j'aimerais utiliser le fournisseur de mémoire en mémoire d'EF Core, mais il s'agit de EF 6. De plus, pour d'autres systèmes de stockage comme RavenDB, je serais également un partisan du test via le fournisseur de base de données en mémoire. Encore une fois - ceci est spécifiquement pour aider à tester le code basé sur EF sans beaucoup de cérémonie .

Voici les objectifs que j'avais lorsque j'ai créé 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
  • Il ne doit pas s'agir de créer d'étranges interfaces multi-responsabilités (telles qu'un modèle de référentiel "générique" ou "typique")
  • Il doit être facile à configurer et à configurer dans un test unitaire

Je suis d'accord avec les déclarations précédentes selon lesquelles EF est toujours un détail de la mise en œuvre et il est correct de penser qu'il est nécessaire de l'abstraire pour pouvoir effectuer un test unitaire "pur". Je conviens également que, dans l'idéal, je voudrais m'assurer que le code EF fonctionne correctement - 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 de EF et créent des tests d'intégration pour tester spécifiquement votre code EF.

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

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

En utilisant l’injection de dépendance (avec ou sans cadre - votre préférence), nous pouvons facilement nous moquer du médiateur et contrôler les mécanismes de requête/réponse pour permettre le code EF de test unitaire.

Tout d'abord, supposons que nous ayons un service doté d'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 explicitement encapsulez tout le code lié à EF dans des classes descriptives, 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 de savoir si les objets pertinents proviennent de EF, MongoDB ou d'un fichier texte.

Maintenant pour la demande 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 constater, l’abstraction est simple et encapsulée. C'est aussi absolument testable car dans un test d'intégration, vous pourriez tester cette classe individuellement - il y a pas de problèmes d'affaires mélangés ici.

Alors, à quoi ressemble un test unitaire de notre service de fonctionnalités? C'est tellement 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 que nous n’avons même pas besoin de configurer quoi que ce soit en plus: c’est un test unitaire très simple. Soyons clairs: Ceci est tout à fait possible de faire sans quelque chose comme Mediatr (vous mettriez simplement en œuvre un interface et mock it pour les tests, par exemple IGetRelevantDbObjectsQuery), mais en pratique pour une grande base de code avec de nombreuses fonctionnalités et requêtes/commandes, j'adore l'encapsulation et le support de DI naturel que Mediatr offre.

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

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

Organiser par tranches de caractéristiques est à côté du point, mais cela permet de garder tous les codes pertinents/dépendants ensemble et facilement découvrables. Plus important encore, je sépare les requêtes par rapport aux commandes - en respectant le principe séparation des commandes/requêtes .

Cela répond à tous mes critères: c’est une cérémonie sobre, facile à comprendre et il ya des avantages cachés supplémentaires. Par exemple, comment gérez-vous l'enregistrement des modifications? Maintenant, vous pouvez simplifier votre contexte de base de données en utilisant une interface de rôle (IUnitOfWork.SaveChangesAsync()) et simuler des appels à l'interface à un seul rôle. Vous pouvez également encapsuler la validation/la restauration dans votre RequestHandlers - comme vous préférez le faire. , tant que c'est maintenable. Par exemple, j’ai été tenté de créer un seul gestionnaire/requête générique, dans lequel vous ne feriez que passer un objet EF et qui le sauvegarderait/le mettrait à jour/le supprimer - mais vous devez demander quelle est votre intention et vous rappeler que si vous le souhaitez Pour échanger le gestionnaire avec un autre fournisseur de stockage/implémentation, vous devez probablement créer des commandes/requêtes explicites qui représentent ce que vous avez l'intention de faire. Plus souvent qu'autrement, un service ou une fonctionnalité unique nécessitera quelque chose de spécifique - ne créez pas de contenu générique avant d'en avoir besoin.

Il y a bien sûr des mises en garde à 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 cogner et tout classer par message - il faudrait attraper de bonnes pratiques de révision du code et des peer reviews. C'est un problème de processus, pas un problème avec MediatR, alors soyez simplement conscient de la façon dont vous utilisez ce modèle.

Vous vouliez un exemple concret de la façon dont les gens testent/moquent EF, et c'est une approche qui fonctionne avec succès pour notre projet - et l'équipe est ravie de la facilité avec laquelle elle est adoptée. J'espère que ça aide! Comme pour tout ce qui concerne la programmation, les approches sont multiples et tout dépend de ce que vous voulez réaliser. J'apprécie la simplicité, la facilité d'utilisation, la facilité de maintenance et la possibilité de découverte - et cette solution répond à toutes ces exigences.

3
kamranicus

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

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

2
Grax