假设我有这个单元测试:
[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属性并在控制器的操作方法中手动执行验证。
我希望我的问题的意图是明确的;有没有办法强制真正的模型绑定执行(包括其验证行为,以测试我没有忘记重要的验证属性)来自自动单元测试?
杰夫
答案 0 :(得分: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)
我同意这不是一个非常令人满意的情况。但是,有一些简单的解决方法:
通过反思数据实体并寻找必要的验证属性(这就是我目前正在做的事情)来解决这个问题。它比听起来容易得多。
构建自己的验证器,该验证器反映viewmodel参数类型并验证它。使用它在单元测试中验证是否设置了正确的验证属性。假设您的验证类没有错误,它应该等同于ASP.NET MVC ModelBinder中的验证算法。我已经为不同的目的编写了这样一个验证器类,它并不比第一个选项困难得多。
答案 2 :(得分: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,如果验证失败等,通过手动添加错误来测试流程是无效的,如您所述。然后,它是对控制器负责的非常可控的测试,而不是框架为您做的事情。
答案 3 :(得分:0)
我不喜欢单独检查属性存在的测试,它使得测试不像文档那样紧密地与我对ASP.NET MVC的理解(这可能是错误的)并且与业务没有紧密结合要求(我关心的)。
因此,对于这些事情,我最终编写集成测试,直接生成HTTP请求或使用WatiN通过浏览器生成。一旦你开始这样做,你就可以在没有额外的MVC抽象的情况下编写测试,测试记录了你真正关心的真实情况。也就是说,这样的测试很慢。
我还做了一些事情,我的集成测试可以发出后门请求,这会导致在服务器进程中加载测试夹具。这个文本夹具会暂时覆盖我的IOC容器中的绑定......这减少了集成测试的设置,尽管它们只是那时的半集成测试。
例如,我可能会使用模拟控制器替换控制器,模拟控制器将验证使用期望参数调用操作方法。更常见的是,我将网站的数据源替换为我预先填充的其他数据源。答案 4 :(得分:0)