MVC 验证的单元测试
-
13-09-2019 - |
题
当我在 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);
}
我想除了这个问题之外, 应该 我正在测试验证,我应该以这种方式测试它吗?
解决方案
相反传递一个BlogPost
的也可以声明的操作参数作为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”时会发生什么。
其他提示
讨厌死灵一个老帖子,但我想我会加入我自己的想法(因为我只是有这个问题,跨越这个帖子跑,同时寻求答案)。
- 请不要在你的控制器测试的测试验证。要么你相信MVC的验证或编写自己的(即不考其他的代码,测试代码)
- 如果你想测试验证是做你所期望的,在你的模型试验测试它(我这样做的一对夫妇我更复杂的正则表达式验证的)。 醇>
你真的要在这里测试什么,你的控制器做你希望它在验证失败时做什么。这是你的代码,您的期望。测试很容易,一旦你意识到这就是你要测试:
[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));
}
我也遇到了同样的问题,在阅读了 Paul 的回答和评论后,我寻找了一种手动验证视图模型的方法。
我发现 本教程 其中解释了如何手动验证使用 DataAnnotations 的 ViewModel。他们的关键代码片段位于帖子的末尾。
我稍微修改了代码 - 在教程中省略了 TryValidateObject 的第四个参数 (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);
}
}
到目前为止,这对我们来说效果非常好。
当你调用homeController.Index方法在您的测试,你不使用任何的MVC框架,触发关闭验证,从而ModelState.IsValid将永远是真实的。在我们的代码,我们直接在控制器,而不是使用环境验证调用辅助验证方法。我还没有与DataAnnotations(我们使用NHibernate.Validators)也许别人可以提供指导如何从控制器中调用验证。很多经验
我今天正在研究这个,我发现 这篇博文 由 Roberto Hernández (MVP) 撰写,这似乎提供了在单元测试期间为控制器操作触发验证器的最佳解决方案。这将在验证实体时将正确的错误放入 ModelState 中。
我使用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,但我会添加它,因为它可能会帮助其他人编写测试他们的控制器:
您没有使用由System.ComponentModel.DataAnnotations提供的验证,但仍使用ViewData.ModelState对象,通过使用其AddModelError
方法和其他一些验证机制的选项。 E.g:
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框架自动地被进行。
我同意,ARM拥有最好的答案:测试你的控制器的行为,而不是内置的验证
但是,可以认为模型/视图模型具有正确的验证特性也单元测试定义。比方说,您的视图模型是这样的:
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,我没有与严重挖掘的问题。因此,这里是我的建议。它建立在贾尔斯·史密斯的答案和ASP.NET MVC4工作(我知道的一个问题是关于MVC 2,但是寻找答案时,谷歌并没有歧视,我不能在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.");
}
这种设置的好处是,我可以重用测试控制器为我所有型号的测试,并能够把它扩大到嘲笑多一点的控制器或使用该控制器具有保护的方法。
希望它能帮助。
如果你关心验证,但你不关心它是如何实现的,如果你只关心抽象的最高级别的动作方法的验证,不管是否如使用DataAnnotations,ModelBinders甚至实现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();
根据@吉尔斯 - 史密斯的回答和评论,对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);
}
}
在答案编辑见上文...
@吉尔斯 - 史密斯的答案是我的首选方法,但实现可以简化为:
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);
}
}