2010-11-11 6 views
3

J'ai fait un modèle personnalisé, et je veux me moquer de lui. Je suis assez nouveau pour MVC, et très nouveau pour les tests unitaires. La plupart des approches que j'ai vu créer une interface pour la classe et ensuite faire un simulacre qui implémente la même interface. Cependant, je n'arrive pas à faire fonctionner cela lorsque je passe l'interface dans la vue. Cue exemple "simplifié":Comment simuler un modèle dans ASP.NET MVC?

Modèle-

public interface IContact 
{ 
    void SendEmail(NameValueCollection httpRequestVars); 
} 

public abstract class Contact : IContact 
{ 
    //some shared properties... 
    public string Name { get; set; } 

    public void SendEmail(NameValueCollection httpRequestVars = null) 
    { 
     //construct email... 
    } 
} 

public class Enquiry : Contact 
{ 
    //some extra properties... 
} 

Affichage-

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<project.Models.IContact>" %> 

<!-- other html... --> 

<td><%= Html.TextBoxFor(model => ((Enquiry)model).Name)%></td> 

Controller-

[HttpPost] 
    public ActionResult Index(IContact enquiry) 
    { 
     if (!ModelState.IsValid) 
      return View(enquiry); 

     enquiry.SendEmail(Request.ServerVariables); 
     return View("Sent", enquiry); 
    } 

Unité Testing

[Test] 
    public void Index_HttpPostInvalidModel_ReturnsDefaultView() 
    { 
     Enquiry enquiry = new Enquiry(); 
     _controller.ModelState.AddModelError("", "dummy value"); 

     ViewResult result = (ViewResult)_controller.Index(enquiry); 

     Assert.IsNullOrEmpty(result.ViewName); 
    } 

    [Test] 
    public void Index_HttpPostValidModel_CallsSendEmail() 
    { 
     MockContact mock = new MockContact(); 

     ViewResult result = (ViewResult)_controller.Index(mock); 

     Assert.IsTrue(mock.EmailSent); 
    } 

public class MockContact : IContact 
{ 
    public bool EmailSent = false; 

    void SendEmail(NameValueCollection httpRequestVars) 
    { 
     EmailSent = true; 
    } 
} 

Lors d'un HttpPost je reçois une exception "Impossible de créer une instance d'une interface". Je semble que je ne peux pas avoir mon gâteau (en passant un modèle) et le manger (passer faux pour les tests unitaires). Peut-être y a-t-il une meilleure approche pour les modèles de tests unitaires liés aux vues?

grâce,

Med

+2

btw, un "modèle" normalement ne contient pas de logique (comme SendEmail()) – Will

+0

N'ouvrons pas la can-de-worms qui est maigre/gros contrôleur/modèle! Où pensez-vous que SendEmail() devrait aller? – med4th

Répondre

9

Je vais jeter là-bas, si vous avez besoin de se moquer de vos modèles que vous faites mal. Vos modèles devraient être des sacs de propriété muets.

Il n'y a absolument aucune raison pour que votre modèle dispose d'une méthode SendEmail. C'est une fonctionnalité qui devrait être appelée depuis un contrôleur appelant vers un EmailService.

En réponse à votre question:

Après des années de travail avec séparation des modèles de Concern (SOC) comme MVC, MVP, MVVM et de voir des articles des gens plus lumineux que moi (je voudrais pouvoir trouver celui que je Je réfléchis à ce sujet mais peut-être que je l'ai lu dans un magazine). Vous finirez par conclure dans une application d'entreprise que vous finirez avec 3 ensembles distincts d'objets de modèle.Auparavant, j'étais un très grand fan de la conception DDD (Domain Driven Design) en utilisant un seul ensemble d'entités commerciales qui étaient à la fois des objets C# plain (POCO) et Persistent Ignorant (PI). Avoir des modèles de domaine qui sont POCO/PI vous laisse avec une liste d'objets propre où il n'y a pas de code lié à l'accès au stockage d'objets ou ayant d'autres attributs qui ont une signification schématique pour seulement 1 zone du code. Bien que cela fonctionne et puisse fonctionner assez bien pendant un certain temps, il existe un point de basculement où la complexité de l'expression de la relation entre Vue, Modèle de domaine et Modèle de stockage physique devient trop complexe pour s'exprimer correctement avec 1. ensemble d'entités.

Pour résoudre les problèmes d'impédance de View, Domain et Storage, vous avez vraiment besoin de 3 jeux de modèles. Vos ViewModels correspondront exactement à vos vues pour faciliter le travail avec l'interface utilisateur. Donc, cela aura souvent des choses telles que l'ajout d'une Liste pour peupler les listes déroulantes avec des valeurs qui sont valables pour votre vue d'édition/action. Au milieu se trouvent les entités de domaine, ce sont les entités que vous devez valider par rapport à vos règles métier. Vous allez donc les mapper depuis/vers les deux côtés de la vue et depuis/vers la couche de stockage. Dans ces entités est où vous pouvez joindre votre code pour faire la validation. Personnellement, je ne suis pas fan de l'utilisation d'attributs et de la logique de validation de couplage dans vos entités de domaine. Cela fait beaucoup de sens de coupler des attributs de validation dans vos ViewModels pour tirer parti de la fonctionnalité de validation côté client MVC intégrée.

Pour la validation, je recommande d'utiliser une bibliothèque comme FluentValidation (ou votre propre custom, ils ne sont pas difficiles à écrire) qui vous permet de séparer vos règles métier de vos objets. Bien qu'avec les nouvelles fonctionnalités de MVC3, vous puissiez effectuer des validations à distance et avoir le côté client, il s'agit d'une option pour gérer la véritable validation de votre activité.

Enfin, vous avez vos modèles de stockage. Comme je l'ai dit précédemment, j'ai été très zélé pour que les objets PI puissent être réutilisés à travers toutes les couches, donc en fonction de la configuration de votre stockage durable, vous pourriez utiliser directement les objets de votre domaine. Mais si vous utilisez des outils tels que Linq2Sql, EntityFramework (EF) etc, vous aurez très probablement des modèles générés automatiquement avec du code pour interagir avec le fournisseur de données, vous aurez donc besoin de mapper vos objets de domaine à vos objets de persistance.

envelopper Alors tout cela jusqu'à ce serait un flux logique standard dans les actions MVC

utilisateur va modifier la fiche produit

  1. EF interroge la base de données pour obtenir les informations de produit existant, à l'intérieur du couche de référentiel Les objets de données EF sont mappés sur les entités métier (BE) de sorte que toutes les méthodes de couche de données renvoient des BE et n'ont aucun couplage externe aux objets de données EF.(Ainsi, si vous changez de fournisseur de données, vous n'avez pas besoin de modifier une seule ligne de code sauf pour l'implémentation interne)

  2. Le contrôleur obtient le produit BE et le mappe sur un Product ViewModel (VM) et ajoute collections pour les différentes options qui peuvent être définies pour les listes déroulantes

  3. Retour Voir (theview, ProductVM)

utilisateur modifie le produit et soumet le formulaire

  1. validation côté client est passé (utile pour la date de validation/numéro de validation au lieu d'avoir à soumettre le formulaire de rétroaction)

  2. Le ProductVM revient mappé à ProductBE à ce stade, vous validerait les règles commerciales le long des lignes Vous passez le ProductBE dans votre modèle de référentiel, dans l'implémentation interne de la couche de données, vous associez le ProductBE à l'Entité de données de produit pour EF et si vous ne le validez pas. mettre à jour la base de données.

2016 edit: usages retirés de Interface que la séparation des préoccupations et des interfaces sont entièrement orthogonal.

+0

Ce patron fonctionne pour moi. Donc, en résumé, ViewModels devrait être "sacs de propriété" et le reste des classes Model fournissent les règles métier pour les utiliser (et garder les contrôleurs maigres)? – med4th

+1

Ayant récemment migré une application de l'utilisation de CodeSmith à Linq2Sql, ce SOC aurait rasé des semaines. C'est dommage que je sois trop nouveau ici pour cogner votre score ;-) – med4th

0

Votre question est ici:

public ActionResult Index(IContact enquiry) 

MVC en arrière-plan doit créer un type concret pour passer à la méthode lors de l'appel. Dans le cas de cette méthode, MVC doit créer un type qui implémente IContract.

Quel type? Je ne sais pas. MVC non plus. Au lieu d'utiliser des interfaces pour pouvoir simuler vos modèles, utilisez des classes normales qui ont des méthodes protégées que vous pouvez remplacer dans les simulacres.

public class Contact 
{ 
    //some shared properties... 
    public string Name { get; set; } 

    public virtual void SendEmail(NameValueCollection httpRequestVars = null) 
    { 
     //construct email... 
    } 
} 

public class MockContact 
{ 
    //some shared properties... 
    public string Name { get; set; } 
    public bool EmailSent {get;private set;} 

    public override void SendEmail(NameValueCollection vars = null) 
    { 
     EmailSent = true; 
    } 
} 

et

public ActionResult Index(Contact enquiry) 
+0

Merci pour la réponse rapide, Will. Comment MockContact est-il lié à Contact? Ne doivent-ils pas être liés, pour être tous les deux passés dans controller.Index (model)? – med4th

Questions connexes