我制作了一个自定义模型,我想嘲笑它。我对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上,我得到一个“无法创建接口的实例”异常。我似乎无法拥有我的蛋糕(通过模型)并吃掉它(通过模拟进行单元测试)。也许对于绑定到视图的单元测试模型有更好的方法吗?
感谢,
地中海
答案 0 :(得分:9)
我要把它扔出去,如果你需要嘲笑你的模型你做错了。你的模特应该是愚蠢的财产袋。
您的模型绝对没有理由应该使用SendEmail方法。这是应该从调用EmailService的控制器调用的功能。
回答您的问题:
经过多年的关注分离(SOC)模式,比如MVC,MVP,MVVM以及看到比我更亮的人的文章(我希望我能找到一个我正在考虑这个的但也许我在一本杂志)。您最终将在企业应用程序中得出结论,最终将得到3组不同的模型对象。
以前,我是使用一组业务实体进行域驱动设计(DDD)的忠实粉丝,这些业务实体都是普通的旧c#对象(POCO)和持久无知(PI)。拥有POCO / PI的域模型会为您留下一块干净的对象,其中没有与访问对象存储相关的代码,或者只有代码的1个区域具有其他具有逻辑意义的属性。
虽然这有效,并且可以在一段时间内运行良好,但最终会出现一个转折点,表达视图,域模型和物理存储模型之间关系的复杂性变得过于复杂,无法正确表达1套实体。
要解决视图,域和存储的阻抗不匹配问题,您确实需要3套模型。您的ViewModel将与您的视图绑定完全匹配,以便于使用UI。所以这经常会有一些事情,例如添加一个List来填充下拉列表,其值对编辑视图/操作有效。
中间是域实体,这些是您应根据业务规则进行验证的实体。因此,您将在视图的两侧与/或从存储层映射到/从它们映射。在这些实体中,您可以附加代码以进行验证。我个人不喜欢使用属性并将验证逻辑耦合到您的域实体中。将验证属性耦合到ViewModel中以利用内置的MVC客户端验证功能确实很有意义。
对于验证,我建议使用像FluentValidation(或您自己的自定义库,它们并不难写)这样的库,它允许您将业务规则与对象分开。虽然使用MVC3的新功能可以进行远程验证,并且可以显示客户端,但这是一个处理真正业务验证的选项。
最后,您拥有存储模型。正如我之前所说,我非常热衷于让PI对象能够在所有层中重复使用,因此根据您设置持久存储的方式,您可以直接使用域对象。但是如果你利用像Linq2Sql,EntityFramework(EF)等工具,你很可能会有自动生成的模型,其中包含与数据提供者交互的代码,因此你需要将域对象映射到持久性对象。
所以将所有这些包装起来这将是MVC操作中的标准逻辑流程
用户转到编辑产品页面
EF查询数据库以获取现有产品信息,在存储库层内,EF数据对象被映射到业务实体(BE),因此所有数据层方法都返回BE并且没有与EF的外部耦合数据对象。 (因此,如果您更改了数据提供者,则除了内部实现之外,您不必更改单行代码)
控制器获取产品BE并将其映射到产品ViewModel(VM),并为可以为下拉列表设置的不同选项添加集合
返回视图(theview,ProductVM)
用户编辑产品并提交表单
传递客户端验证(对于日期验证/数字验证很有用,而不必提交表单以供反馈)
此时,ProductVM将映射回ProductBE,您将验证ValidationFactory.Validate(ProductBE)
行的业务规则,如果无效则返回查看和取消编辑的消息,否则继续
您将ProductBE传递到您的存储库模型,在您将ProductBE映射到EF的产品数据实体的数据层的内部实现中,并更新数据库。
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)
可以使用接口。