Unit tests on MVC validation

前端 未结 12 1208
离开以前
离开以前 2020-12-04 06:31

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 Previe

相关标签:
12条回答
  • 2020-12-04 07:00

    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.

    0 讨论(0)
  • 2020-12-04 07:02

    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.

    0 讨论(0)
  • 2020-12-04 07:05

    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);
    }
    
    0 讨论(0)
  • 2020-12-04 07:05

    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();
    
    0 讨论(0)
  • 2020-12-04 07:10

    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.

    0 讨论(0)
  • 2020-12-04 07:12

    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);
            }
    
    0 讨论(0)
提交回复
热议问题