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

时间:2010-11-11 17:17:50

标签: asp.net-mvc unit-testing mocking

我制作了一个自定义模型,我想嘲笑它。我对MVC很新,对单元测试也很陌生。我见过的大多数方法都为类创建了一个接口,然后创建一个实现相同接口的模拟器。但是,当实际将接口传递到View时,我似乎无法使其工作。提示“简化”的例子:

模型 -

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

查看 -

<%@ 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上,我得到一个“无法创建接口的实例”异常。我似乎无法拥有我的蛋糕(通过模型)并吃掉它(通过模拟进行单元测试)。也许对于绑定到视图的单元测试模型有更好的方法吗?

感谢,

地中海

3 个答案:

答案 0 :(得分:9)

我要把它扔出去,如果你需要嘲笑你的模型你做错了。你的模特应该是愚蠢的财产袋。

您的模型绝对没有理由应该使用SendEmail方法。这是应该从调用EmailService的控制器调用的功能。

回答您的问题:

经过多年的关注分离(SOC)模式,比如MVC,MVP,MVVM以及看到比我更亮的人的文章(我希望我能找到一个我正在考虑这个的但也许我在一本杂志)。您最终将在企业应用程序中得出结论,最终将得到3组不同的模型对象。

以前,我是使用一组业务实体进行域驱动设计(DDD)的忠实粉丝,这些业务实体都是普通的旧c#对象(PO​​CO)和持久无知(PI)。拥有POCO / PI的域模型会为您留下一块干净的对象,其中没有与访问对象存储相关的代码,或者只有代码的1个区域具有其他具有逻辑意义的属性。

虽然这有效,并且可以在一段时间内运行良好,但最终会出现一个转折点,表达视图,域模型和物理存储模型之间关系的复杂性变得过于复杂,无法正确表达1套实体。

要解决视图,域和存储的阻抗不匹配问题,您确实需要3套模型。您的ViewModel将与您的视图绑定完全匹配,以便于使用UI。所以这经常会有一些事情,例如添加一个List来填充下拉列表,其值对编辑视图/操作有效。

中间是域实体,这些是您应根据业务规则进行验证的实体。因此,您将在视图的两侧与/或从存储层映射到/从它们映射。在这些实体中,您可以附加代码以进行验证。我个人不喜欢使用属性并将验证逻辑耦合到您的域实体中。将验证属性耦合到ViewModel中以利用内置的MVC客户端验证功能确实很有意义。

对于验证,我建议使用像FluentValidation(或您自己的自定义库,它们并不难写)这样的库,它允许您将业务规则与对象分开。虽然使用MVC3的新功能可以进行远程验证,并且可以显示客户端,但这是一个处理真正业务验证的选项。

最后,您拥有存储模型。正如我之前所说,我非常热衷于让PI对象能够在所有层中重复使用,因此根据您设置持久存储的方式,您可以直接使用域对象。但是如果你利用像Linq2Sql,EntityFramework(EF)等工具,你很可能会有自动生成的模型,其中包含与数据提供者交互的代码,因此你需要将域对象映射到持久性对象。

所以将所有这些包装起来这将是MVC操作中的标准逻辑流程

用户转到编辑产品页面

  1. EF查询数据库以获取现有产品信息,在存储库层内,EF数据对象被映射到业务实体(BE),因此所有数据层方法都返回BE并且没有与EF的外部耦合数据对象。 (因此,如果您更改了数据提供者,则除了内部实现之外,您不必更改单行代码)

  2. 控制器获取产品BE并将其映射到产品ViewModel(VM),并为可以为下拉列表设置的不同选项添加集合

  3. 返回视图(theview,ProductVM)

  4. 用户编辑产品并提交表单

    1. 传递客户端验证(对于日期验证/数字验证很有用,而不必提交表单以供反馈)

    2. 此时,ProductVM将映射回ProductBE,您将验证ValidationFactory.Validate(ProductBE)行的业务规则,如果无效则返回查看和取消编辑的消息,否则继续

      < / LI>
    3. 您将ProductBE传递到您的存储库模型,在您将ProductBE映射到EF的产品数据实体的数据层的内部实现中,并更新数据库。

    4. 2016编辑:删除了Interface的用法,因为关注点和界面的分离是完全正交的。

答案 1 :(得分: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)

答案 2 :(得分:0)

可以使用接口。

请参阅:http://mvcunity.codeplex.com/