web-dev-qa-db-fra.com

Première architecture ASP.NET MVC3 et Entity Framework Code

Ma question précédente m'a fait repenser les couches, le dépôt, l'injection de dépendances et les trucs architecturaux comme celui-ci.

Mon architecture ressemble maintenant à ceci:
J'utilise d'abord le code EF, donc je viens de créer des classes POCO et du contexte. Cela crée db et modèle.
Les classes de couche métier (fournisseurs) sont de niveau supérieur. 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, j'obtiens des données et je les envoie à Views.

Mon architecture initiale comprenait un référentiel, dont je me suis débarrassé car on m'a dit qu'il ne faisait qu'ajouter de la complexité, alors pourquoi je n'utilise pas seulement EF uniquement. Je voulais faire ça .. 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 - 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 sais pas comment procéder correctement.

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

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

53
Damb

Si vous souhaitez 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 le contexte ou le référentiel se moquant crée simplement un test qui peut tester votre logique de couche supérieure (qui utilise ces modèles) mais pas votre application.

Exemple simple:

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

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

Et permet d'écrire une méthode commerciale:

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

Maintenant, si vous vous moquez du 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 les index n'est pas prise en charge dans L2E.

C'était un exemple simple mais il peut en être de même avec l'utilisation de méthodes dans les requêtes et autres erreurs courantes. De plus, cela affecte également des méthodes comme Ajouter, Mettre à jour, Supprimer généralement exposées sur le référentiel. Si vous n'écrivez pas de maquette qui simulera exactement le comportement du contexte EF et l'intégrité référentielle, vous ne testerez pas votre implémentation.

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

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

Les tests d'intégration ont du 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 IQueryable mais retournera directement des 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 masquées dans cette classe et non 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 supplémentaire - par exemple, ajouter modèle de spécification pour pouvoir définir la requête dans la couche supérieure.

Dans ASP.NET MVC, vous pouvez remplacer partiellement 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. Le nombre et les types de tests nécessaires dépendent de la complexité de votre application, de l'avenir attendu de l'application, de vos compétences et des compétences des autres membres de l'équipe.

De petits projets directs peuvent être créés sans tests (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 franchit un certain seuil, vous pouvez constater que l'introduction de nouvelles fonctionnalités ou la maintenance du projet est très dur parce que vous n'êtes jamais sûr qu'il casse quelque chose qui a déjà fonctionné - 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 devraient idéalement couvrir tous les chemins d'exécution de la méthode. Ces tests doivent être très courts et faciles à écrire - une partie compliquée peut être de configurer des dépendances (mocks, faktes, stubs).
  • Les tests d'intégration vous aident à tester les fonctionnalités sur plusieurs couches et généralement sur plusieurs processus (application, base de données). Vous n'avez pas besoin de les avoir pour tout, il s'agit plutôt de l'expérience pour sélectionner où ils sont utiles.
  • Les tests de bout en bout sont quelque chose comme la validation du cas d'utilisation/de la user story/de la fonctionnalité. Ils devraient couvrir l'ensemble du flux de l'exigence.

Il n'est pas nécessaire de tester une fermeture plusieurs fois - si vous savez que la fonctionnalité est testée dans un test de bout en bout, vous n'avez pas besoin d'écrire un test d'intégration pour le même code. De plus, si vous savez que cette méthode n'a qu'un seul chemin d'exécution 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ù vous commencez avec un gros test (de bout en bout ou d'intégration) et approfondissez les tests unitaires.

Selon votre approche de développement, vous n'avez pas à commencer par plusieurs types de tests depuis le début, mais vous pouvez les introduire plus tard car votre application deviendra plus complexe. L'exception est TDD/BDD où vous devez commencer à utiliser au moins des tests de bout en bout et unitaires avant même d'écrire une seule ligne d'un autre code.

Vous posez donc la mauvaise question. La question n'est pas ce qui est plus simple? La question est de savoir ce qui vous aidera à la fin et quelle complexité correspond à votre application? Si vous souhaitez que l'application et la logique métier soient testées facilement, 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 expliquer l'exemple de mon projet précédent:

J'ai travaillé sur le 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émentielle et itérative. Il y avait beaucoup de logique métier compliquée et beaucoup de requêtes de base de données compliquées. Nous avons commencé avec des référentiels génériques et une couverture de code élevée avec des tests unitaires + tests d'intégration pour valider le mappage (tests simples pour insérer, supprimer, mettre à jour et sélectionner une entité). Après quelques mois, nous avons constaté que notre approche ne fonctionnait pas. Nous avons eu 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. Changer quoi que ce soit dans le modèle EF pourrait introduire des problèmes inattendus dans des pièces qui n'ont pas été touchées pendant plusieurs semaines. Nous avons constaté qu'il manquait des tests d'intégration ou des tests de bout en bout pour notre logique d'application. La même conclusion a été faite sur 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 de nouveaux projets.

93
Ladislav Mrnka

L'utilisation du modèle de référentiel ajoute-t-elle de la complexité? Dans votre scénario, je ne pense pas. Cela rend 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 TDD et les modèles de conception dans Entity Framework, consultez: http://msdn.Microsoft.com/en-us/ff714955.aspx

Cependant, il semble que vous recherchiez une approche pour simuler Entity Framework. Une solution consisterait à utiliser une méthode d'amorçage virtuelle pour générer des données sur l'initialisation de la base de données. Jetez un œil à la section Seed sur: 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 célèbres que je connaisse sont:

Pour voir une liste plus complète des frameworks de simulation .NET, consultez: 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 . Étudiez davantage sur 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 se réfèrent à Entity Framework 4.0. Mais vous aurez 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 voie à suivre pour simuler ma couche de base de données dans un test unitaire?

13
Kamyar

Ce que je fais, c'est que j'utilise un simple objet ISession et EFSession, il est facile de se moquer de mon contrôleur, facile d'accès avec Linq et fortement typé. Injectez avec DI à l'aide de 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, je n'ai qu'à faire une MongoSession qui implémente ISession ...

2
VinnyG

J'avais le même problème pour décider de la conception générale de mon application MVC. This Le projet CodePlex de Shiju Varghese m'a beaucoup aidé. Cela se fait dans ASP.net MVC3, EF CodeFirst et utilise également une couche de service et une couche de référentiel. L'injection de dépendances se fait à l'aide de Unity. C'est simple et très facile à suivre. Il est également soutenu par environ 4 articles de blog très Nice. Cela vaut la peine de vérifier. Et, n'abandonnez pas le référentiel.

1
Ben