如何在ASP.NET MVC中模拟一个模型?

3
我制作了一个自定义模型,想要模拟它。我对MVC相当新,对单元测试非常新。我看到的大多数方法都是为类创建一个接口,然后创建一个实现相同接口的模拟。但是,当将接口实际传递给视图时,我似乎无法让它起作用。以下是一个“简化”的示例:
模型-
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...
}

View-

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

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

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

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

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

单元测试 -

    [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;
    }
}

在HttpPost期间,我遇到了“无法创建接口实例”的异常。看起来我不能同时传递模型和模拟对象进行单元测试。也许有更好的方法来对绑定视图的模型进行单元测试?谢谢,Med。

2
顺便说一下,“模型”通常不包含逻辑(例如SendEmail())。 - user1228
让我们不要打开“瘦/胖控制器/模型”的潘多拉魔盒!你认为SendEmail()应该放在哪里? - med4th
3个回答

9
我想说的是,如果您需要模拟您的模型,那么您正在做错事情。您的模型应该是简单的属性包,绝对没有理由让您的模型拥有SendEmail方法。这种功能应该通过控制器调用EmailService来实现。
回答您的问题:多年来,我一直使用分离关注点(SOC)模式(如MVC、MVP和MVVM),并阅读过比我更聪明的人们的文章(我希望我能找到我所思考的那篇文章,但可能是我在杂志上读到的)。最终,您将得出一个结论,在企业应用程序中,您将最终拥有3个不同的模型对象集。
以前,我非常喜欢使用领域驱动设计(DDD)使用单一的业务实体集,这些实体既是普通的C#对象(POCO),也是持久的无知(PI)。拥有POCO/PI的领域模型会为您留下一个干净的对象板,其中没有与访问对象存储或具有仅对代码的1个区域具有图示意义的其他属性相关的代码。
虽然这很有效,并且在一段时间内可以很好地发挥作用,但最终将出现表达视图、领域模型和物理存储模型之间关系的复杂度过高而无法正确表达的转折点。
要解决视图、领域和存储的阻抗不匹配,您确实需要3个模型集。您的ViewModel将完全匹配您的视图绑定,以便易于与UI一起使用。因此,它经常包括添加列表以填充下拉列表,并提供适用于编辑视图/操作的有效值。
在中间是领域实体,这些实体应该根据业务规则进行验证。因此,您将在双方(从视图和存储层)映射到/从它们,并在这些实体中可以附加代码以执行验证。个人而言,我不喜欢使用属性并将验证逻辑耦合到您的领域实体中。将验证属性耦合到您的ViewModel中利用内置的MVC客户端验证功能是有很多意义的。
对于验证,我建议使用FluentValidation等库(或者编写您自己的自定义库),使您能够将业务规则与对象分离。尽管使用MVC3的新功能,您可以远程验证服务器端并在客户端显示,但这是处理真正业务验证的一种选择。
最后,您还有存储模型。正如我之前所说,我非常热衷于让PI对象能够通过所有层进行重用,因此,根据您如何设置持久存储,您可能可以直接使用领域对象。但是,如果您利用像Linq2Sql、EntityFramework(EF)等工具,您很可能会有带有与数据提供程序交互的代码的自动生成模型,因此您需要将领域对象映射到持久性对象。
因此,在MVC操作中,所有这些都将是标准的逻辑流程。
用户进入编辑产品页面
  1. EF从数据库中获取现有的产品信息,在存储库层内,EF数据对象映射到业务实体(BE),因此所有数据层方法返回BE,并且没有外部耦合到EF数据对象。(因此,如果您更改数据提供程序,除了内部实现之外,无需修改任何代码)
  2. 控制器获取Product BE并将其映射到Product ViewModel (VM),并添加用于下拉列表中可设置的不同选项的集合
  3. 返回视图(theview, ProductVM)

用户编辑产品并提交表单

  1. 客户端验证通过(对于日期验证/数字验证非常有用,而不是必须提交表单以获得反馈)
  2. 在此时,ProductVM被映射回ProductBE,您需要沿着这条线验证业务规则ValidationFactory.Validate(ProductBE),如果无效,则返回消息到视图并取消编辑,否则继续
  3. 将ProductBE传递给您的存储库模型,在数据层的内部实现中,将ProductBE映射到EF的Product Data Entity并更新数据库。

2016年编辑: 删除了使用接口作为关注点分离和接口完全正交的用法。


那个模式对我很有效。因此,总结一下,ViewModel应该是“属性包”,而其他Model类提供业务规则来利用它们(并保持控制器的瘦身)? - med4th
1
最近将一个应用程序从使用CodeSmith迁移到Linq2Sql,如果采用SOC的话,可以节省数周时间。可惜我在这里还太新了,无法提高您的分数;-) - med4th

0

你的问题在这里:

public ActionResult Index(IContact enquiry)

MVC在调用方法时需要创建一个具体类型来传递。在这个方法的情况下,MVC需要创建一个实现IContract接口的类型。

是哪种类型?我不知道。MVC也不知道。

不要使用接口来模拟您的模型,而是使用具有受保护方法的普通类,您可以在模拟中重写这些方法。

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;
    }
}

并且

public ActionResult Index(Contact enquiry)

感谢您的快速回复,威尔。MockContact和Contact有什么关系呢?它们不是必须要有关联才能一起传递给controller.Index(model)吗? - med4th

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接