78

当我在 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);
}

我想除了这个问题之外,我应该测试验证,我应该以这种方式测试它吗?

4

12 回答 12

194

讨厌删除旧帖子,但我想我会添加自己的想法(因为我刚刚遇到这个问题并在寻找答案时遇到了这篇帖子)。

  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));
}
于 2010-09-28T19:02:35.903 回答
91

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

我发现本教程解释了如何手动验证使用 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);
        }
    }

到目前为止,这对我们来说非常有效。

于 2010-07-28T13:08:08.883 回答
7

当您在测试中调用 homeController.Index 方法时,您没有使用任何触发验证的 MVC 框架,因此 ModelState.IsValid 将始终为真。在我们的代码中,我们直接在控制器中调用辅助 Validate 方法,而不是使用环境验证。我对 DataAnnotations(我们使用 NHibernate.Validators)没有太多经验,也许其他人可以提供指导如何从您的控制器中调用 Validate。

于 2009-08-13T03:58:12.283 回答
3

我今天正在研究这个,我发现Roberto Hernández (MVP) 的这篇博客文章似乎提供了在单元测试期间触发验证器以执行控制器操作的最佳解决方案。这将在验证实体时将正确的错误放入 ModelState 中。

于 2010-10-05T03:45:24.927 回答
2

我在我的测试用例中使用 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);
        }
于 2010-02-18T19:50:14.710 回答
1

这并不能完全回答您的问题,因为它放弃了 DataAnnotations,但我会添加它,因为它可能会帮助其他人为其控制器编写测试:

您可以选择不使用 System.ComponentModel.DataAnnotations 提供的验证,但仍使用 ViewData.ModelState 对象,方法是使用其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);
    }
}

这仍然可以让您利用Html.ValidationMessageFor()MVC 生成的东西,而无需使用DataAnnotations. 您必须确保您使用的密钥与AddModelError视图对验证消息的期望相匹配。

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

于 2010-09-23T19:52:22.573 回答
1

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

但是,您也可以对您的模型/视图模型定义正确的验证属性进行单元测试。假设您的 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);
}
于 2012-05-26T16:41:41.143 回答
1

与ARM相比,我对挖坟没有问题。所以这是我的建议。它建立在 Giles Smith 的答案之上,适用于 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.");
    }

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

希望能帮助到你。

于 2013-03-21T20:38:10.200 回答
1

如果你关心验证但你不关心它是如何实现的,如果你只关心你的动作方法在最高抽象级别的验证,不管它是使用 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();
于 2015-08-01T22:04:32.650 回答
0

基于 @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);
        }
    }

请参阅上面的答案编辑...

于 2015-03-30T20:10:42.627 回答
0

@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);
        }
    }
于 2017-02-09T08:42:24.523 回答
-4

除了传入 a 之外BlogPost,您还可以将 actions 参数声明为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”时会发生什么。

于 2009-08-13T07:32:09.027 回答