MVC 验证的单元测试

发布于 2024-07-30 20:10:02 字数 938 浏览 14 评论 0原文

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

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

How can I test that my controller action is putting the correct errors in the ModelState when validating an entity, when I'm using DataAnnotation validation in MVC 2 Preview 1?

Some code to illustrate. First, the action:

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

And here's a failing unit test that I think should be passing but isn't (using 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);
}

I guess in addition to this question, should I be testing validation, and should I be testing it in this way?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(12

楠木可依 2024-08-06 20:10:02

我也遇到了同样的问题,在阅读了 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);
        }
    }

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

I had been having the same problem, and after reading Pauls answer and comment, I looked for a way of manually validating the view model.

I found this tutorial which explains how to manually validate a ViewModel that uses DataAnnotations. They Key code snippet is towards the end of the post.

I amended the code slightly - in the tutorial the 4th parameter of the TryValidateObject is omitted (validateAllProperties). In order to get all the annotations to Validate, this should be set to true.

Additionaly I refactored the code into a generic method, to make testing of ViewModel validation simple:

    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);
        }
    }

So far this has worked really well for us.

复古式 2024-08-06 20:10:02

讨厌破坏旧帖子,但我想我应该添加自己的想法(因为我刚刚遇到这个问题并在寻求答案时遇到了这篇文章)。

  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));
}

Hate to necro a old post, but I thought I'd add my own thoughts (since I just had this problem and ran across this post while seeking the answer).

  1. Don't test validation in your controller tests. Either you trust MVC's validation or write your own (i.e. don't test other's code, test your code)
  2. If you do want to test validation is doing what you expect, test it in your model tests (I do this for a couple of my more complex regex validations).

What you really want to test here is that your controller does what you expect it to do when validation fails. That's your code, and your expectations. Testing it is easy once you realize that's all you want to test:

[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));
}
顾挽 2024-08-06 20:10:02

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

I was researching this today and I found this blog post by Roberto Hernández (MVP) that seems to provide the best solution to fire the validators for a controller action during unit testing. This will put the correct errors in the ModelState when validating an entity.

阳光下的泡沫是彩色的 2024-08-06 20:10:02

这并不能完全回答您的问题,因为它放弃了 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);
    }
}

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

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

This doesn't exactly answer your question, because it abandons DataAnnotations, but I'll add it because it might help other people write tests for their Controllers:

You have the option of not using the validation provided by System.ComponentModel.DataAnnotations but still using the ViewData.ModelState object, by using its AddModelError method and some other validation mechanism. 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);
    }
}

This still lets you take advantage of the Html.ValidationMessageFor() stuff that MVC generates, without using the DataAnnotations. You have to make sure the key you use with AddModelError matches what the view is expecting for validation messages.

The controller then becomes testable because the validation is happening explicitly, rather than being done automagically by the MVC framework.

嗼ふ静 2024-08-06 20:10:02

我同意 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);
}

I agree that ARM has the best answer: test the behaviour of your controller, not the built-in validation.

However, you can also unit test that your Model/ViewModel has the correct validation attributes defined. Let's say your ViewModel looks like this:

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

This unit test will test for the existence of the [Required] attribute:

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

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

    Assert.IsNotNull(attribute);
}
黎歌 2024-08-06 20:10:02

与ARM相比,我没有挖坟的问题。 这是我的建议。 它建立在 Giles Smith 的答案之上,适用于 ASP.NET MVC4(我知道问题是关于 MVC 2 的,但 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.");
    }

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

希望能帮助到你。

In contrast to ARM, I don't have a problem with grave digging. So here is my suggestion. It builds on the answer of Giles Smith and works for ASP.NET MVC4 (I know the question is about MVC 2, but Google doesn't discriminate when looking for answers and I cannot test on MVC2.)
Instead of putting the validation code in a generic static method, I put it in a test controller. The controller has everything needed for validation. So, the test controller looks like this:

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);
            }
        }
    }

Of course the class does not need to be a protected innerclass, that is the way I use it now but I probably am going to reuse that class. If somewhere there is a model MyModel that is decorated with nice data annotation attributes, then the test looks something like this:

    [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.");
    }

The advantage of this setup is that I can reuse the test controller for tests of all my models and may be able to extend it to mock a bit more about the controller or use the protected methods that a controller has.

Hope it helps.

春花秋月 2024-08-06 20:10:02

如果您关心验证但不关心它是如何实现的,如果您只关心最高抽象级别的操作方法的验证,无论它是使用 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();

If you care about validation but you don't care about how it is implemented, if you only care about validation of your action method at the highest level of abstraction, no matter whether it is implemented as using DataAnnotations, ModelBinders or even ActionFilterAttributes, then you could use Xania.AspNet.Simulator nuget package as follows:

install-package Xania.AspNet.Simulator

--

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

modelState.IsValid.Should().BeFalse();
黯淡〆 2024-08-06 20:10:02

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

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

Based on @giles-smith 's answer and comments, for 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);
        }
    }

See on answer edit above...

淡淡の花香 2024-08-06 20:10:02

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

@giles-smith's answer is my preferred approach but the implementation can be simplified:

    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);
        }
    }
×纯※雪 2024-08-06 20:10:02

您还可以将 actions 参数声明为 FormCollection,而不是传入 BlogPost。 然后您可以自己创建 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”时会发生什么。

Instead of passing in a BlogPost you can also declare the actions parameter as FormCollection. Then you can create the BlogPost yourself and call UpdateModel(model, formCollection.ToValueProvider());.

This will trigger the validation for any field in the 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);
    }

Just make sure your test adds a null value for every field in the views form that you want to leave empty.

I found that doing it this way, at the expense of a few extra lines of code, makes my unit tests resemble the way the code gets called at runtime more closely making them more valuable. Also you can test what happens when someone enters "abc" in a control bound to an int property.

划一舟意中人 2024-08-06 20:10:02

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

When you call the homeController.Index method in your test, you aren't using any of the MVC framework that fires off the validation so ModelState.IsValid will always be true. In our code we call a helper Validate method directly in the controller rather than using ambient validation. I haven't had much experience with the DataAnnotations (We use NHibernate.Validators) maybe someone else can offer guidance how to call Validate from within your controller.

雾里花 2024-08-06 20:10:02

我在测试用例中使用 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);
        }

I'm using ModelBinders in my test cases to be able to update model.IsValid value.

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

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

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

With my MvcModelBinder.BindModel method as follows (basically the same code used
internally in the MVC framework):

        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);
        }
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文