2010-10-07 134 views
1

比方说,我有这样的单元测试:TDD和MVC模型绑定

[Test] 
    public void LastNameShouldNotBeEmpty() 
    { 
     ExampleController controller = new ExampleController(); 

     Person editedPerson = new Person { FirstName = "j", LastName = "" }; 
     controller.EditPerson(editedPerson); 

     Assert.AreEqual(controller.ModelState.IsValid, false); 
    } 

而这种代码:

public class ExampleController : Controller 
{ 
    public ActionResult EditPerson(int personId) 
    { 
     // Serve up a view, whatever 
     return View(Person.LoadPerson(personId)); 
    } 

    [HttpPost] 
    public ActionResult EditPerson(Person person) 
    { 
     if (ModelState.IsValid) 
     { 
      // TODO - actually save the modified person, whatever 
     } 

     return View(person); 
    } 
} 

public class Person 
{ 
    public string FirstName { get; set; } 
    [Required] public string LastName { get; set; } 
} 

它困扰着我,如果我TDD了一个要求,即姓氏不能是空的,我不能满足使用DataAnnotation属性的测试(在Person上的LastName声明之前的[Required]),因为当从单元测试调用控制器的操作方法时,MVC基础结构没有机会应用在模型绑定过程中进行验证。

(如果我手动在控制器的EditPerson方法进行验证,不过,并添加一个错误的ModelState中,这将是从一个单元测试验证。)

我缺少的东西?我想指定使用单元测试我的系统的验证行为,但我不知道如何满足一个单元测试,除非我完全放弃DataAnnotation属性和手动执行内我的控制器的操作方法验证。

我希望我的问题的意图是明确的;有没有办法强制真正的模型绑定从自动单元测试执行(包括验证行为,以测试我没有忘记重要的验证属性)?

杰夫

回答

0

我个人认为你应该有单元测试来测试MVC范围之外的属性本身。这应该是你的模型测试的一部分,而不是你的控制器测试。你没有写MVC验证码,所以不要试着去测试它!只要测试你的对象具有你期望的正确属性的事实。

这是很粗糙的,但你的想法...

[Test] 
public void LastNameShouldBeRequired() 
{ 
    var personType = typeof(Person); 
    var lastNamePropInfo = objType.GetProperty("LastName"); 
    var requiredAttrs = lastNamePropInfo.GetCustomAttributes(typeof(RequiredAttribute), true).OfType<RequiredAttribute>(); 
    Assert.IsTrue(requiredAttrs.Any()); 
} 

然后在你的MVC测试你只是测试控制器的流量,而不是数据注解的有效性。你可以告诉ModelState中,它是无效的测试如果验证通过手动添加一个错误,因为你注意到失败等发生了什么流量。然后,这是对你的控制器负责的一个非常可控的测试,而不是框架为你做什么。

+0

我明白你在说什么,我当然不需要测试MVC基础结构代码的正确行为。我要测试的是,我记得要添加正确的属性,而不是基础设施是否正确验证它们(因为我相信它确实)。你的解决方案也可以。我只是不想为基于批注的验证编写完全不同的单元测试,而不是使用动作方法中的自定义逻辑实现的验证。我想出了一个替代方案,可以让我以相同的方式指定两个测试。 – blaster 2010-10-08 17:44:24

+0

我认为最好使用更自然的方式,例如测试使用验证器隔离实体(它是自己的单元测试)。恕我直言,通过检查注释来做这件事不是一种自然的单元测试方式。 – Braulio 2012-03-08 17:08:12

1

我同意这不是一个非常令人满意的情况。不过,也有一些简单的解决办法:解决此问题

  1. 工作通过反映在数据实体和寻找必要的验证特性(这是我目前在做什么)。它比听起来容易得多。

  2. 建立自己的验证,反映了视图模型参数类型和验证它。用它来验证你的单元测试是否设置了适当的验证属性。假设您的验证类是无缺陷的,它应该等同于ASP.NET MVC ModelBinder中的验证算法。我已经为了一个不同的目的而编写了这样一个验证器类,它不比第一个选项困难得多。

0

我不喜欢那个检查个人属性的存在的测试中,它使测试作用不像文档和紧密结合我的ASP.NET MVC的理解(这可能是错误的),而不是紧密地结合在一起业务需求(我关心的)。

因此对于这类事情,我最终编写了集成测试,直接或通过浏览器使用WatiN生成HTTP请求。一旦你完成了这个任务,你可以在没有额外的MVC抽象的情况下编写测试,测试记录你真正关心的是什么。也就是说,这样的测试很慢。

我也做过一些事情,我的集成测试可以发出后门请求,这会导致在服务器进程中加载​​测试夹具。这个文本夹具将临时覆盖我的IOC容器中的绑定......这样可以减少集成测试的设置,尽管在这一点上它们只是半集成测试。

例如,我可能会用一个模拟控制器替换一个控制器,该控制器将验证用期望的参数调用动作方法。更通常的情况下,我将网站的数据源替换为我预先填充的另一个数据源。

7

下面是我提出的一个解决方案。它要求将一行代码添加到单元测试中,但是我发现它让我不在乎是否通过操作方法中的通过自定义代码的属性强制实施验证,这感觉像测试更精神指定结果而不是实施。即使验证来自数据注释,它也允许测试以书面形式传递。请注意,新行权EditPerson动作方法的调用上面:

[Test] 
    public void LastNameShouldNotBeEmpty() 
    { 
     FakeExampleController controller = new FakeExampleController(); 

     Person editedPerson = new Person { FirstName = "j", LastName = "" }; 

     // Performs the same attribute-based validation that model binding would perform 
     controller.ValidateModel(editedPerson); 

     controller.EditPerson(editedPerson); 

     Assert.AreEqual(false, controller.ModelState.IsValid); 
     Assert.AreEqual(true, controller.ModelState.Keys.Contains("LastName")); 
     Assert.AreEqual("Last name cannot be blank", controller.ModelState["LastName"].Errors[0].ErrorMessage); 
    } 

ValidateModel实际上是我创建一个扩展方法(控制器确实有ValidateModel方法,但它是受保护的,因此它不能被调用从单元测试直接)。它使用反射调用控制器上的受保护的TryValidateModel()方法,该方法将触发基于注释的验证,就像动作方法确实通过MVC.NET基础结构调用一样。

public static class Extensions 
{ 
    public static void ValidateModel<T>(this Controller controller, T modelObject) 
    { 
     if (controller.ControllerContext == null) 
      controller.ControllerContext = new ControllerContext(); 

     Type type = controller.GetType(); 
     MethodInfo tryValidateModelMethod = 
      type.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).Where(
       mi => mi.Name == "TryValidateModel" && mi.GetParameters().Count() == 1).First(); 

     tryValidateModelMethod.Invoke(controller, new object[] {modelObject}); 
    } 
} 

它似乎能够以最小的侵入性工作,尽管可能存在我不知道的后果。 。 。

杰夫

+1

+1 - 调用TryValidateModel为我工作,虽然我去了派生控制器类而不是反射。顺便说一句,你不需要BindingFlags.Public,该方法不一定是通用的。 – 2010-12-31 17:28:23

0

我们可以利用Validator辅助类做TDD与模型验证。你可以找到一个详细的博客关于测试驾驶模型验证here