MVC验证的单元测试

时间:2009-08-13 02:20:49

标签: asp.net-mvc unit-testing validation asp.net-mvc-2 tdd

当我在MVC 2预览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);
}

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

12 个答案:

答案 0 :(得分:190)

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

  1. 不要在控制器测试中测试验证。您要么信任MVC的验证,要么自己编写(即不测试其他代码,测试代码)
  2. 如果您确实希望测试验证是按照您的期望进行的,请在模型测试中进行测试(我这样做是为了进行一些更复杂的正则表达式验证)。
  3. 您真正想要测试的是,当验证失败时,您的控制器会按预期执行操作。这是你的代码,也是你的期望。一旦你意识到你想要测试的全部内容,就很容易进行测试:

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

答案 1 :(得分:85)

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

我找到了this tutorial,它解释了如何手动验证使用DataAnnotations的ViewModel。他们的密钥代码片段即将发布。

我稍微修改了代码 - 在教程中省略了TryValidateObject的第4个参数(validateAllProperties)。为了将所有注释都设置为Validate,应将其设置为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);
        }
    }

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

答案 2 :(得分:6)

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

答案 3 :(得分:3)

我今天正在研究这个,我发现RobertoHernández(MVP)的this blog post似乎提供了在单元测试期间触发控制器动作的验证器的最佳解决方案。这将在验证实体时将正确的错误放在ModelState中。

答案 4 :(得分:2)

我在我的测试用例中使用ModelBinder来更新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);
        }

答案 5 :(得分:1)

这并不完全回答你的问题,因为它放弃了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框架自动完成。

答案 6 :(得分:1)

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

但是,您也可以单元测试您的Model / 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);
}

答案 7 :(得分: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.");
    }

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

希望它有所帮助。

答案 8 :(得分: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();

答案 9 :(得分: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);
        }
    }

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

答案 10 :(得分: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);
        }
    }

答案 11 :(得分:-3)

您也可以将actions参数声明为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”时会发生什么。