MVC 验证的单元测试

当我在 MVC 2 Preview 1中使用 DataAnnotation 验证时,如何测试我的控制器操作在验证实体时在 ModelState 中放置了正确的错误?

一些代码来说明。首先,操作:

    [HttpPost]
public ActionResult Index(BlogPost b)
{
if(ModelState.IsValid)
{
_blogService.Insert(b);
return(View("Success", b));
}
return View(b);
}

这里有一个失败的单元测试,我认为应该通过,但是没有通过(使用 MbUnit & Moq) :

[Test]
public void When_processing_invalid_post_HomeControllerModelState_should_have_at_least_one_error()
{
// arrange
var mockRepository = new Mock<IBlogPostSVC>();
var homeController = new HomeController(mockRepository.Object);


// act
var p = new BlogPost { Title = "test" };            // date and content should be required
homeController.Index(p);


// assert
Assert.IsTrue(!homeController.ModelState.IsValid);
}

我想除了这个问题,应该我正在测试验证,我应该用这种方式测试它吗?

26693 次浏览

当你打电话给家庭控制器。在测试中,如果没有使用任何启动验证的 MVC 框架,那么 ModelState.IsValid 将始终为 true。在我们的代码中,我们直接在控制器中调用辅助验证方法,而不是使用环境验证。我没有太多使用 DataAnnotions 的经验(我们使用 NHibernate。Validators)也许其他人可以提供如何在控制器中调用 Validator 的指导。

不需要传入 BlogPost,您也可以将 action 参数声明为 FormCollection。然后您可以自己创建 BlogPost并调用 UpdateModel(model, formCollection.ToValueProvider());

这将触发对 FormCollection中任何字段的验证。

    [HttpPost]
public ActionResult Index(FormCollection form)
{
var b = new BlogPost();
TryUpdateModel(model, form.ToValueProvider());


if (ModelState.IsValid)
{
_blogService.Insert(b);
return (View("Success", b));
}
return View(b);
}

只需确保您的测试为视图表单中希望保持为空的每个字段添加一个空值。

我发现这样做的代价是牺牲一些额外的代码行,使我的单元测试类似于代码在运行时被调用的方式,使它们更有价值。还可以测试当某人在绑定到 int 属性的控件中输入“ abc”时会发生什么。

我在测试用例中使用 ModelBinders 来更新 model. IsValid 值。

var form = new FormCollection();
form.Add("Name", "0123456789012345678901234567890123456789");


var model = MvcModelBinder.BindModel<AddItemModel>(controller, form);


ViewResult result = (ViewResult)controller.Add(model);

使用我的 MvcModelBinder.BindModel 方法,如下所示(基本上使用相同的代码 在 MVC 框架内部) :

        public static TModel BindModel<TModel>(Controller controller, IValueProvider valueProvider) where TModel : class
{
IModelBinder binder = ModelBinders.Binders.GetBinder(typeof(TModel));
ModelBindingContext bindingContext = new ModelBindingContext()
{
FallbackToEmptyPrefix = true,
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(TModel)),
ModelName = "NotUsedButNotNull",
ModelState = controller.ModelState,
PropertyFilter = (name => { return true; }),
ValueProvider = valueProvider
};


return (TModel)binder.BindModel(controller.ControllerContext, bindingContext);
}

我也遇到过同样的问题,在阅读了 Paul 的回答和评论之后,我开始寻找一种手动验证视图模型的方法。

我找到了 本教程,它解释了如何手动验证使用 DataAnnotations 的 ViewModel。他们的关键代码片段是接近文章的结尾。

我稍微修改了代码——在教程中省略了 TryValidateObject 的第4个参数(validateAllProperties)。为了使所有的注释都有效,应该将这个值设置为 true。

此外,我还将代码重构为一个通用方法,以简化 ViewModel 验证的测试:

    public static void ValidateViewModel<TViewModel, TController>(this TController controller, TViewModel viewModelToValidate)
where TController : ApiController
{
var validationContext = new ValidationContext(viewModelToValidate, null, null);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(viewModelToValidate, validationContext, validationResults, true);
foreach (var validationResult in validationResults)
{
controller.ModelState.AddModelError(validationResult.MemberNames.FirstOrDefault() ?? string.Empty, validationResult.ErrorMessage);
}
}

到目前为止,我们做得很好。

这并不能准确地回答你的问题,因为它放弃了 DataAnnotions,但是我将添加它,因为它可能会帮助其他人为他们的 Controller 编写测试:

您可以选择不使用 System 提供的验证。组件模型。但仍在使用 ViewData。对象,方法是使用其 AddModelError方法和其他一些验证机制。例如:

public ActionResult Create(CompetitionEntry competitionEntry)
{
if (competitionEntry.Email == null)
ViewData.ModelState.AddModelError("CompetitionEntry.Email", "Please enter your e-mail");


if (ModelState.IsValid)
{
// insert code to save data here...
// ...


return Redirect("/");
}
else
{
// return with errors
var viewModel = new CompetitionEntryViewModel();
// insert code to populate viewmodel here ...
// ...




return View(viewModel);
}
}

这仍然允许您利用 MVC 生成的 Html.ValidationMessageFor()内容,而不需要使用 DataAnnotations。您必须确保与 AddModelError一起使用的键与视图对验证消息的期望值相匹配。

然后控制器变得可测试,因为验证是显式发生的,而不是由 MVC 框架自动完成的。

我讨厌旧文章,但是我想我应该加入我自己的想法(因为我刚刚遇到了这个问题,并且在寻找答案的时候偶然发现了这篇文章)。

  1. 不要在控制器测试中测试验证。你要么相信 MVC 的验证,要么自己编写(例如,不测试别人的代码,测试自己的代码)
  2. 如果您确实希望测试验证正在执行您所期望的任务,那么在您的模型测试中对其进行测试(对于一些更复杂的正则表达式验证,我是这样做的)。

您真正想在这里测试的是,当验证失败时,您的控制器执行您期望它执行的操作。这是你的准则,也是你的期望。一旦你意识到这就是你想要测试的全部,测试就变得简单了:

[test]
public void TestInvalidPostBehavior()
{
// arrange
var mockRepository = new Mock<IBlogPostSVC>();
var homeController = new HomeController(mockRepository.Object);
var p = new BlogPost();


homeController.ViewData.ModelState.AddModelError("Key", "ErrorMessage"); // Values of these two strings don't matter.
// What I'm doing is setting up the situation: my controller is receiving an invalid model.


// act
var result = (ViewResult) homeController.Index(p);


// assert
result.ForView("Index")
Assert.That(result.ViewData.Model, Is.EqualTo(p));
}

今天我研究了这个问题,发现佛斯托·卡蒙纳 这篇博文(mVP)似乎提供了在单元测试期间启动控制器操作验证器的最佳解决方案。这将在验证实体时将正确的错误放入 ModelState 中。

我同意 ARM 有最好的答案: 测试您的控制器的行为,而不是内置的验证。

但是,您也可以单元测试您的 Model/ViewModel 是否定义了正确的验证属性。假设您的 ViewModel 是这样的:

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

这个单元测试将测试 [Required]属性的存在:

[TestMethod]
public void FirstName_should_be_required()
{
var propertyInfo = typeof(PersonViewModel).GetProperty("FirstName");


var attribute = propertyInfo.GetCustomAttributes(typeof(RequiredAttribute), false)
.FirstOrDefault();


Assert.IsNotNull(attribute);
}

与 ARM 不同的是,我对挖掘坟墓没有任何问题。所以我的建议是。它建立在 Giles Smith 的答案之上,并且适用于 ASP.NET MVC4(我知道这个问题是关于 MVC2的,但是 Google 在寻找答案时没有歧视,我不能在 MVC2上测试) 我没有将验证代码放在通用的静态方法中,而是将它放在测试控制器中。控制器具有验证所需的一切。因此,测试控制器看起来像这样:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Wbe.Mvc;


protected class TestController : Controller
{
public void TestValidateModel(object Model)
{
ValidationContext validationContext = new ValidationContext(Model, null, null);
List<ValidationResult> validationResults = new List<ValidationResult>();
Validator.TryValidateObject(Model, validationContext, validationResults, true);
foreach (ValidationResult validationResult in validationResults)
{
this.ModelState.AddModelError(String.Join(", ", validationResult.MemberNames), validationResult.ErrorMessage);
}
}
}

当然这个类不需要是一个受保护的内部类,这是我现在使用它的方式,但是我可能会重用这个类。如果在某个地方有一个用漂亮的数据注释属性装饰的模型 MyModel,那么测试看起来就像这样:

    [TestMethod()]
public void ValidationTest()
{
MyModel item = new MyModel();
item.Description = "This is a unit test";
item.LocationId = 1;


TestController testController = new TestController();
testController.TestValidateModel(item);


Assert.IsTrue(testController.ModelState.IsValid, "A valid model is recognized.");
}

这种设置的优点是,我可以重用测试控制器来测试我的所有模型,并且可以扩展它来模拟更多关于控制器的内容,或者使用控制器所具有的受保护的方法。

希望能有帮助。

基于@giles-smith 的回答和评论,Web API:

    public static void ValidateViewModel<TViewModel, TController>(this TController controller, TViewModel viewModelToValidate)
where TController : ApiController
{
var validationContext = new ValidationContext(viewModelToValidate, null, null);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(viewModelToValidate, validationContext, validationResults, true);
foreach (var validationResult in validationResults)
{
controller.ModelState.AddModelError(validationResult.MemberNames.FirstOrDefault() ?? string.Empty, validationResult.ErrorMessage);
}
}

参见上面的回答编辑..。

如果你关心验证,但你不关心它是如何实现的,如果你只关心在最高抽象级别的操作方法的验证,不管它是以使用 DataAnnotations、 ModelBinders 甚至 ActionFilterAttritribute 的方式实现的,那么你可以使用 Xania。AspNet.模拟器 Nuget 软件包如下:

install-package Xania.AspNet.Simulator

--

var action = new BlogController()
.Action(c => c.Index(new BlogPost()), "POST");
var modelState = action.ValidateRequest();


modelState.IsValid.Should().BeFalse();

@ giles-smith 的回答是我更喜欢的方法,但实现可以简化:

    public static void ValidateViewModel(this Controller controller, object viewModelToValidate)
{
var validationContext = new ValidationContext(viewModelToValidate, null, null);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(viewModelToValidate, validationContext, validationResults, true);
foreach (var validationResult in validationResults)
{
controller.ModelState.AddModelError(validationResult.MemberNames.FirstOrDefault() ?? string.Empty, validationResult.ErrorMessage);
}
}