关于MVC验证的单元测试


77

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

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


5
是var p =新的BlogPost {标题=“ test”}; 安排多于行动?
RichardOD

1
Assert.IsFalse(homeController.ModelState.IsValid);
赛斯·

Answers:


-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”时发生的情况。


2
我喜欢这种方法,但似乎倒退了一步,或者在处理POST的每个动作中至少必须多执行一个步骤。
马修·格罗夫斯

2
我同意。但是让我的单元测试和真实的应用程序以相同的方式工作是值得的。
莫里斯

5
ARM的方法更好,恕我直言:)
kamranicus 2011年

4
这种打败MVC的目的。
安迪

2
我同意ARM的答案更好。与传递强类型的Model / ViewModel对象相比,不希望将FormCollection传递给控制器​​操作。
亚历克斯·约克

193

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

  1. 不要在控制器测试中测试验证。您可以信任MVC的验证,也可以编写自己的验证(即,不测试其他人的代码,而是测试您的代码)
  2. 如果您确实想测试验证是否达到了预期的效果,请在模型测试中对其进行测试(我为几个更复杂的regex验证执行此操作)。

您真正想要在此处进行测试的是,控制器在验证失败时会执行您期望的操作。那就是您的代码和您的期望。一旦意识到这就是要测试的全部,就可以轻松进行测试:

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

2
我同意,这应该是正确的答案。正如ARM所说:内置验证不应进行测试。相反,控制器的行为应该是经过测试的东西。这是最合理的。
亚历克斯·约克

控制器应与模型绑定和验证分开进行测试。遵循KISS和关注分离。我在这里做了有关单元测试MVC组件的一小部分文章timoch.com/blog/2013/06/…– TiMoch 2013
6

3
为了测试自定义验证属性,您应该怎么做?如果正在使用这些,则不能“信任MVC的验证”。您将如何(假设在模型测试中)测试自定义验证是否有效?
约翰·桑德斯

2
我不同意。我们仍然需要验证给定的模型是否会产生该测试中用作前提条件的模型错误。然而,示例代码是您在1中定义的问题的完美答案。但是,它并不是最初问题的答案
Ibrahim ben Salah

这不是测试模型验证。举例来说,某人可以(有意或无意)删除模型中的数据注释(可能是合并错误?),并且该测试不会失败。
Rosdi Kasim

89

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

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


对不起,我什至没有检查。我们所有的MVC项目都在4.0版本中
Giles Smith 2010年

谢谢你!小附录 如果您的验证未与特定字段关联(例如,您已实现IValidatableObject),则MemberNames为空,并且模型错误键应为空字符串。在foreach中,您可以执行以下操作:var key = validationResult.MemberNames.Any() ? validationResult.MemberNames.First() : string.Empty; controller.ModelState.AddModelError(key, validationResult.ErrorMessage);
ThomasLundström

6
为什么需要使用泛型?如果将其定义为:void ValidateViewModel(object viewModelToValidate,Controller controller),或者甚至更好地将其作为扩展方法,则可以更轻松地使用它:public static void ValidateViewModel(this Controller controller,object viewModelToValidate)
乍得·格兰特

这很棒,但是我同意乍得只是摆脱通用语法。
罗杰

如果有人对我的“ Validator”存在相同的问题,请使用“ System.ComponentModel.DataAnnotations.Validator.TryValidateObject”来确保使用正确的Validator。
Alin Ciocan '16

7

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


我喜欢“环境验证”一词。但是,是否必须有一种方法可以在单元测试中触发它?
马修·格罗夫斯

3
但是,问题在于您基本上是在测试MVC框架-而不是控制器。您试图确认MVC正在按您的期望验证模型。唯一可以确定的方法是模拟整个MVC管道并模拟Web请求。这可能比您真正需要知道的要多。如果您只是在测试是否正确设置了模型上的数据验证,则可以在没有控制器的情况下执行此操作,而只需手动运行数据验证即可。
Paul Alexander

3

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


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

如果您在一个属性上具有多个验证属性,则此方法不起作用。controller.ModelState.Clear();在创建的代码之前添加此行ModelBindingContext,它将起作用
Suhas 2012年

1

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

您可以选择不使用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框架自动完成的。


以这种方式进行验证会丢弃MVC中验证的某些最佳部分。我想在模型上而不是在控制器中添加验证。如果使用此解决方案,我将在噩梦中最终得到很多可能的代码重复。
Willem Meints 2012年

@ W.Meints:是的,但是如果您愿意,也可以将上述示例中用于验证的代码行移至Model的方法中。关键是,通过代码而不是通过属性进行验证使其更具可测试性。保罗在stackoverflow.com/a/1269960/22194上进行
代码与标准

1

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

但是,您也可以对模型/ 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);
}

那么我们将如何测试内置验证?特别是如果我们使用额外的属性,错误消息等对它进行了自定义
Teoman shipahi 2015年

1

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

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

希望能帮助到你。


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

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

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


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);
        }
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.