【问题标题】:TDD and MVC Model BindingTDD 和 MVC 模型绑定
【发布时间】:2011-08-17 20:39:46
【问题描述】:

假设我有这个单元测试:

    [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 出 LastName 不能为空的要求,我就无法满足使用 DataAnnotation 属性(Person 的 LastName 声明之前的 [Required])的测试,因为当控制器的操作方法从单元测试中调用时,MVC 基础架构没有机会应用它在模型绑定期间所做的验证。

(不过,如果我在控制器的 EditPerson 方法中手动执行验证,并向 ModelState 添加了一个错误,则可以通过单元测试进行验证。)

我错过了什么吗?我想使用单元测试指定系统的验证行为,但我不确定如何满足单元测试,除非我完全放弃 DataAnnotation 属性并在控制器的操作方法中手动执行验证。

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

杰夫

【问题讨论】:

    标签: asp.net-mvc tdd


    【解决方案1】:

    这是我想出的一种解决方案。它要求将一行代码添加到单元测试中,但我发现它让我不在乎是否通过属性强制执行验证,或者通过 action 方法中的自定义代码,感觉测试更像是精神上的指定结果而不是执行。即使验证来自数据注释,它也允许测试以书面形式通过。请注意,新行位于调用 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 - 调用 TryValidateModel 对我有用,尽管我选择了派生控制器类而不是反射。顺便说一句,您不需要 BindingFlags.Public 并且该方法不必是通用的。
    【解决方案2】:

    我同意这不是一个非常令人满意的情况。但是,有一些简单的解决方法:

    1. 通过反映数据实体并寻找必要的验证属性(这就是我目前正在做的)来解决这个问题。这比听起来容易得多。

    2. 构建您自己的验证器,以反映视图模型参数类型并对其进行验证。使用它在您的单元测试中验证是否设置了正确的验证属性。假设您的验证类没有错误,它应该等同于 ASP.NET MVC ModelBinder 中的验证算法。我为不同的目的编写了这样一个验证器类,它并不比第一个选项难多少。

    【讨论】:

      【解决方案3】:

      我个人认为,在 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 测试中,您只测试控制器的流程,而不是数据注释的有效性。正如您所指出的,您可以通过手动添加错误来告诉模型状态,如果验证失败等情况下测试发生的流程是无效的。然后这是一个非常受控的测试,你的控制器负责什么,而不是框架为你做什么。

      【讨论】:

      • 我明白你在说什么,我当然不需要测试 MVC 基础结构代码的正确行为。我要测试的是我记得添加正确的属性,而不是基础设施是否正确验证它们(因为我相信它确实如此)。您的解决方案也可以。我只是不想为碰巧基于注释的验证编写完全不同的单元测试,而不是碰巧在操作方法中使用自定义逻辑实现的验证。我想出了一个替代方案,可以让我以相同的方式指定两个测试。
      • 我认为最好使用更自然的方式,例如测试使用验证器隔离实体(它自己的单元测试)。恕我直言,通过检查注释来做到这一点不是单元测试的自然方式。
      【解决方案4】:

      我不喜欢亲自检查属性是否存在的测试,它使测试不像文档,并且与我对 ASP.NET MVC 的理解紧密耦合(这可能是错误的),而不是与业务紧密耦合要求(我关心)。

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

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

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

      【讨论】:

        【解决方案5】:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-07-08
          • 1970-01-01
          • 2012-06-24
          相关资源
          最近更新 更多