MVCvalidationunit testing
当我在MVC 2预览版1中使用DataAnnotationvalidation时,如何testing我的控制器操作在validation实体时是否在ModelState中input了正确的错误?
一些代码来说明。 首先,行动:
[HttpPost] public ActionResult Index(BlogPost b) { if(ModelState.IsValid) { _blogService.Insert(b); return(View("Success", b)); } return View(b); }
这是一个失败的unit testing,我认为应该通过,但不是(使用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); }
我想除了这个问题,我应该testingvalidation,我应该以这种方式进行testing吗?
您也可以将操作参数声明为FormCollection
而不是传入BlogPost
。 然后你可以自己创buildBlogPost
并调用UpdateModel(model, formCollection.ToValueProvider());
。
这将触发FormCollection
任何字段的validation。
[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); }
只要确保您的testing为视图表单中的每个字段添加了一个空值,并将其保留为空。
我发现这样做是以牺牲一些额外的代码为代价的,这使得我的unit testing类似于代码在运行时被调用的方式,使得它们更有价值。 你也可以testing当某人在绑定到int属性的控件中input“abc”时会发生什么。
恨恨一个旧的post,但我想我会加上自己的想法(因为我只是有这个问题,跑过这个post,而寻求答案)。
- 不要在你的控制器testing中testingvalidation。 要么你信任MVC的validation或写你自己的(即不要testing其他的代码,testing你的代码)
- 如果你想testingvalidation正在做你的期望,testing它在你的模型testing(我这样做几个我更复杂的正则expression式validation)。
你真正想在这里testing的是,当你的控制器在validation失败时做你期望的事情。 这是你的代码和你的期望。 一旦你意识到这一切你想testing,testing它很容易:
[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)); }
我一直有同样的问题,并在阅读保罗回答和评论后,我寻找一种手动validation视图模型的方式。
我发现本教程介绍了如何手动validation使用DataAnnotations的ViewModel。 他们的关键代码片段是朝向post的结尾。
我稍微修改了代码 – 在教程中,TryValidateObject的第四个参数被省略(validateAllProperties)。 为了获得所有的注释来validation,这应该被设置为true。
另外我把代码重构成一个通用的方法,使ViewModelvalidationtesting变得简单:
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); } }
到目前为止,这对我们非常有效。
当你在你的testing中调用homeController.Index方法时,你没有使用任何引发validation的MVC框架,所以ModelState.IsValid将始终为真。 在我们的代码中,我们直接在控制器中调用一个辅助方法Validate方法,而不是使用环境validation。 我没有太多的DataAnnotations(我们使用NHibernate.Validators)的经验,也许别人可以提供指导,如何从您的控制器调用validation。
我今天正在研究这个,我发现了RobertoHernández(MVP)的这篇博客文章 ,似乎提供了在unit testing期间为控制器操作激发validation器的最佳解决scheme。 这将validation实体时在ModelState中input正确的错误。
我在我的testing用例中使用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); }
这并不完全回答你的问题,因为它放弃了DataAnnotations,但我会添加它,因为它可能会帮助其他人为他们的控制器编写testing:
您可以select不使用由System.ComponentModel.DataAnnotations提供的validation,但仍使用ViewData.ModelState对象,通过使用它的AddModelError
方法和其他validation机制。 例如:
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
中使用的密钥与视图期望validation消息的密钥相匹配。
然后控制器变成可testing的,因为validation是明确发生的,而不是由MVC框架自动完成的。
我同意ARM有最好的答案:testing你的控制器的行为,而不是内置的validation。
不过,你也可以unit testing你的Model / ViewModel是否有正确的validation属性定义。 假设您的ViewModel如下所示:
public class PersonViewModel { [Required] public string FirstName { get; set; } }
这个unit testing将testing[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相比,我没有严重的挖掘问题。 所以这里是我的build议。 它build立在Giles Smith的答案上,适用于ASP.NET MVC4(我知道这个问题是关于MVC 2的,但Google在查找答案时无法区分,而且我无法在MVC2上进行testing)。一个通用的静态方法,我把它放在一个testing控制器。 控制器拥有validation所需的一切。 所以,testing控制器看起来像这样:
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,装饰了很好的数据注释属性,那么testing看起来像这样:
[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."); }
这种设置的好处是我可以重新使用testing控制器来testing我所有的模型,并且可以扩展它来模拟更多关于控制器或使用控制器所具有的保护方法。
希望能帮助到你。
如果你关心validation,但是你不关心它是如何实现的,如果你只关心在最高抽象层次上validation你的动作方法,不pipe它是否被实现为使用DataAnnotations,ModelBinder甚至ActionFilterAttributes你可以使用Xania.AspNet.Simulator 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的回答和评论,对于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); } }
请参阅上面的回答编辑…
@ 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); } }