53 votes

ASP.NET MVC3 et Entity Framework Architecture de type "Code first".

Ma question précédente m'a fait réfléchir à nouveau sur les couches, le référentiel, l'injection de dépendances et les trucs architecturaux de ce genre.

Mon architecture ressemble maintenant à ceci :
J'utilise d'abord le code EF, donc j'ai juste créé des classes POCO, et le contexte. Cela crée la base de données et le modèle.
Au niveau supérieur, on trouve les classes de la couche métier (Providers). J'utilise un fournisseur différent pour chaque domaine... comme MemberProvider, RoleProvider, TaskProvider etc. et je crée une nouvelle instance de mon DbContext dans chacun de ces fournisseurs.
Ensuite, j'instancie ces fournisseurs dans mes contrôleurs, je récupère des données et je les envoie à Views.

Mon architecture initiale comprenait un référentiel, dont je me suis débarrassé parce qu'on m'a dit qu'il ne faisait qu'ajouter de la complexité, alors pourquoi je n'utilise pas uniquement EF. Je voulais le faire travailler avec EF directement à partir des contrôleurs, mais je dois écrire des tests et c'était un peu compliqué avec une vraie base de données. J'ai dû simuler des données d'une manière ou d'une autre. J'ai donc créé une interface pour chaque fournisseur et créé de faux fournisseurs avec des données codées en dur dans des listes. Et avec cela, je suis revenu à quelque chose, où je ne suis pas sûr de savoir comment procéder correctement.

Ces choses commencent à être trop vite compliquées... de nombreuses approches et "patterns"... cela crée trop de bruit et de code inutile.

Existe-t-il une architecture SIMPLE et testable pour créer une application ASP.NET MVC3 avec Entity Framework ?

93voto

Ladislav Mrnka Points 218632

Si vous voulez utiliser TDD (ou toute autre approche de test avec une couverture de test élevée) et EF ensemble, vous devez écrire des tests d'intégration ou de bout en bout. Le problème ici est que toute approche avec mocking soit le contexte ou le référentiel crée juste un test qui peut tester votre logique de couche supérieure (qui utilise ces mocks) mais pas votre application.

Un exemple simple :

Définissons le référentiel générique :

public interface IGenericRepository<TEntity> 
{
    IQueryable<TEntity> GetQuery();
    ...
}

Et écrivons une méthode de travail :

public IEnumerable<MyEntity> DoSomethingImportant()
{
    var data = MyEntityRepo.GetQuery().Select((e, i) => e);
    ...
}

Maintenant, si vous simulez le référentiel, vous utiliserez Linq-To-Objects et vous aurez un test vert, mais si vous exécutez l'application avec Linq-To-Entities, vous obtiendrez une exception car la surcharge de sélection avec des index n'est pas supportée par L2E.

Il s'agissait d'un exemple simple, mais la même chose peut se produire avec l'utilisation de méthodes dans les requêtes et d'autres erreurs courantes. De plus, cela affecte également les méthodes telles que Add, Update, Delete habituellement exposées dans le référentiel. Si vous n'écrivez pas un objet fantaisie qui simule exactement le comportement du contexte EF et l'intégrité référentielle, vous ne pourrez pas tester votre implémentation.

Une autre partie de l'histoire concerne les problèmes de chargement paresseux qui peuvent aussi difficilement être détectés avec des tests unitaires contre des mocks.

Pour cette raison, vous devriez également introduire des tests d'intégration ou des tests de bout en bout qui fonctionneront sur une base de données réelle en utilisant un contexte EF réel et L2E. En fait, l'utilisation de tests de bout en bout est nécessaire pour utiliser correctement le TDD. Pour écrire des tests de bout en bout en ASP.NET MVC, vous pouvez WatiN et peut-être aussi SpecFlow pour BDD mais cela ajoutera beaucoup de travail mais vous aurez votre application vraiment testée. Si vous voulez en savoir plus sur le TDD, je vous recommande ce livre (le seul inconvénient est que les exemples sont en Java).

Les tests d'intégration ont un sens si vous n'utilisez pas de référentiel générique et que vous cachez vos requêtes dans une classe qui n'exposera pas les données. IQueryable mais renvoie directement les données.

Exemple :

public interface IMyEntityRepository
{
    MyEntity GetById(int id);
    MyEntity GetByName(string name); 
}

Maintenant, vous pouvez simplement écrire un test d'intégration pour tester l'implémentation de ce référentiel car les requêtes sont cachées dans cette classe et ne sont pas exposées aux couches supérieures. Mais ce type de référentiel est en quelque sorte considéré comme une ancienne implémentation utilisée avec des procédures stockées. Vous perdrez beaucoup de fonctionnalités ORM avec cette implémentation ou vous devrez faire beaucoup de travail additionnel - par exemple ajouter schéma de spécifications pour pouvoir définir une requête dans la couche supérieure.

Dans ASP.NET MVC, vous pouvez partiellement remplacer les tests de bout en bout par des tests d'intégration au niveau du contrôleur.

Modifier en fonction du commentaire :

Je ne dis pas que vous avez besoin de tests unitaires, de tests d'intégration et de tests de bout en bout. Je dis que faire des applications testées demande beaucoup plus d'efforts. La quantité et les types de tests nécessaires dépendent de la complexité de votre application, de l'avenir prévu de l'application, de vos compétences et de celles des autres membres de l'équipe.

Les petits projets simples peuvent être créés sans aucun test (ok, ce n'est pas une bonne idée mais nous l'avons tous fait et à la fin cela a fonctionné) mais une fois qu'un projet a franchi un certain seuil, vous pouvez constater que l'introduction de nouvelles fonctionnalités ou la maintenance du projet est très difficile parce que vous n'êtes jamais sûr de ne pas casser quelque chose qui fonctionnait déjà - c'est ce qu'on appelle la régression. La meilleure défense contre la régression est un bon ensemble de tests automatisés.

  • Les tests unitaires vous aident à tester la méthode. Ces tests doivent idéalement couvrir tous les chemins d'exécution de la méthode. Ces tests doivent être très courts et faciles à écrire - la partie la plus compliquée peut être la mise en place de dépendances (mocks, faktes, stubs).
  • Les tests d'intégration vous aident à tester la fonctionnalité à travers plusieurs couches et généralement à travers plusieurs processus (application, base de données). Vous n'avez pas besoin d'en avoir pour tout, c'est plutôt une question d'expérience pour choisir où ils sont utiles.
  • Les tests de bout en bout sont quelque chose comme la validation du cas d'utilisation / de l'histoire de l'utilisateur / de la fonctionnalité. Ils doivent couvrir l'ensemble du flux des exigences.

Il n'est pas nécessaire de tester une fonctionnalité plusieurs fois - si vous savez que la fonctionnalité est testée dans le test de bout en bout, vous n'avez pas besoin d'écrire un test d'intégration pour le même code. De même, si vous savez que la méthode n'a qu'un seul chemin d'exécution qui est couvert par le test d'intégration, vous n'avez pas besoin d'écrire un test unitaire pour elle. Cela fonctionne beaucoup mieux avec l'approche TDD où l'on commence par un gros test (de bout en bout ou d'intégration) et on va plus loin dans les tests unitaires.

En fonction de votre approche du développement, vous n'êtes pas obligé de commencer avec plusieurs types de tests dès le début, mais vous pouvez les introduire plus tard, lorsque votre application deviendra plus complexe. L'exception est TDD/BDD où vous devriez commencer à utiliser au moins des tests de bout en bout et des tests unitaires avant même d'écrire une seule ligne de code.

Vous posez donc la mauvaise question. La question n'est pas de savoir ce qui est plus simple. La question est de savoir ce qui vous aidera à la fin et quelle complexité convient à votre application. Si vous voulez avoir une application et une logique d'entreprise facilement testées en unité, vous devez envelopper le code EF dans d'autres classes qui peuvent être simulées. Mais en même temps, vous devez introduire d'autres types de tests pour vous assurer que le code EF fonctionne.

Je ne peux pas vous dire quelle approche conviendra à votre environnement / projet / équipe / etc. Mais je peux vous donner des exemples tirés de mes projets passés :

J'ai travaillé sur ce projet pendant environ 5-6 mois avec deux collègues. Le projet était basé sur ASP.NET MVC 2 + jQuery + EFv4 et il a été développé de manière incrémentale et itérative. Il comportait une logique commerciale complexe et des requêtes de base de données compliquées. Nous avons commencé par des référentiels génériques et une couverture de code élevée avec des tests unitaires + des tests d'intégration pour valider le mappage (tests simples pour l'insertion, la suppression, la mise à jour et la sélection d'entités). Après quelques mois, nous avons constaté que notre approche ne fonctionnait pas. Nous avions plus de 1.200 tests unitaires, une couverture de code d'environ 60% (ce qui n'est pas très bon) et beaucoup de problèmes de régression. Tout changement dans le modèle EF pouvait introduire des problèmes inattendus dans des parties qui n'avaient pas été touchées pendant plusieurs semaines. Nous avons découvert qu'il nous manquait des tests d'intégration ou des tests de bout en bout pour la logique de notre application. La même conclusion a été faite par une équipe parallèle travaillant sur un autre projet et l'utilisation de tests d'intégration a été considérée comme une recommandation pour les nouveaux projets.

13voto

Kamyar Points 11012

L'utilisation du modèle de référentiel ajoute-t-elle de la complexité ? Dans votre scénario, je ne le pense pas. Il rend le TDD plus facile et votre code plus facile à gérer. Essayez d'utiliser un modèle de référentiel générique pour plus de séparation et un code plus propre.

Si vous souhaitez en savoir plus sur le TDD et les modèles de conception dans Entity Framework, jetez un coup d'œil à la page suivante : http://msdn.microsoft.com/en-us/ff714955.aspx

Cependant, il semble que vous cherchiez une approche pour tester à blanc Entity Framework. Une solution serait d'utiliser une méthode de semence virtuelle pour générer des données lors de l'initialisation de la base de données. Jetez un coup d'œil à Semences section à : http://blogs.msdn.com/b/adonet/archive/2010/09/02/ef-feature-ctp4-dbcontext-and-databases.aspx

Vous pouvez également utiliser certains cadres de simulation. Les plus connus que je connais sont :

Pour obtenir une liste plus complète des cadres de simulation .NET, consultez le site : https://stackoverflow.com/questions/37359/what-c-mocking-framework-to-use

Une autre approche serait d'utiliser un fournisseur de base de données en mémoire comme SQLite . Pour en savoir plus, consultez le site Existe-t-il un fournisseur en mémoire pour Entity Framework ?

Enfin, voici quelques bons liens sur les tests unitaires d'Entity Framework (certains liens font référence à Entity Framework 4.0. Mais vous aurez compris l'idée) :

http://social.msdn.microsoft.com/Forums/en/adodotnetentityframework/thread/678b5871-bec5-4640-a024-71bd4d5c77ff

http://mosesofegypt.net/post/Introducing-Entity-Framework-Unit-Testing-with-TypeMock-Isolator.aspx

Quelle est la marche à suivre pour simuler ma couche de base de données dans un test unitaire ?

2voto

VinnyG Points 2996

Ce que je fais, c'est que j'utilise un simple objet ISession et EFSession, qui sont faciles à simuler dans mon contrôleur, faciles à accéder avec Linq et fortement typés. Injection avec DI en utilisant Ninject.

public interface ISession : IDisposable
    {
        void CommitChanges();
        void Delete<T>(Expression<Func<T, bool>> expression) where T : class, new();
        void Delete<T>(T item) where T : class, new();
        void DeleteAll<T>() where T : class, new();
        T Single<T>(Expression<Func<T, bool>> expression) where T : class, new();
        IQueryable<T> All<T>() where T : class, new();
        void Add<T>(T item) where T : class, new();
        void Add<T>(IEnumerable<T> items) where T : class, new();
        void Update<T>(T item) where T : class, new();
    }

public class EFSession : ISession
    {
        DbContext _context;

        public EFSession(DbContext context)
        {
            _context = context;
        }

        public void CommitChanges()
        {
            _context.SaveChanges();
        }

        public void Delete<T>(System.Linq.Expressions.Expression<Func<T, bool>> expression) where T : class, new()
        {

            var query = All<T>().Where(expression);
            foreach (var item in query)
            {
                Delete(item);
            }
        }

        public void Delete<T>(T item) where T : class, new()
        {
            _context.Set<T>().Remove(item);
        }

        public void DeleteAll<T>() where T : class, new()
        {
            var query = All<T>();
            foreach (var item in query)
            {
                Delete(item);
            }
        }

        public void Dispose()
        {
            _context.Dispose();
        }

        public T Single<T>(System.Linq.Expressions.Expression<Func<T, bool>> expression) where T : class, new()
        {
            return All<T>().FirstOrDefault(expression);
        }

        public IQueryable<T> All<T>() where T : class, new()
        {
            return _context.Set<T>().AsQueryable<T>();
        }

        public void Add<T>(T item) where T : class, new()
        {
            _context.Set<T>().Add(item);
        }

        public void Add<T>(IEnumerable<T> items) where T : class, new()
        {
            foreach (var item in items)
            {
                Add(item);
            }
        }

        /// <summary>
        /// Do not use this since we use EF4, just call CommitChanges() it does not do anything
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="item"></param>
        public void Update<T>(T item) where T : class, new()
        {
            //nothing needed here
        }

Si je veux passer de EF4 à, disons, MongoDB, il me suffit de créer une MongoSession qui implémente ISession...

1voto

Ben Points 569

J'avais le même problème pour décider de la conception générale de mon application MVC. Ce site Le projet CodePlex de Shiju Varghese m'a beaucoup aidé. Il est réalisé en ASP.net MVC3, EF CodeFirst et utilise également une couche service et une couche référentiel. L'injection de dépendances est faite en utilisant Unity. C'est simple et très facile à suivre. Il est également soutenu par quatre articles de blog très intéressants. Il vaut la peine d'être consulté. Et n'abandonnez pas le référentiel pour l'instant.

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