Unit tests on MVC validation
Asked Answered
E

12

78

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?

Ecker answered 13/8, 2009 at 2:20 Comment(2)
Isn't var p = new BlogPost { Title = "test" }; more Arrange than Act?Azure
Assert.IsFalse(homeController.ModelState.IsValid);Arabist
B
-4

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.

Buckley answered 13/8, 2009 at 7:32 Comment(6)
I like this approach, but it seems like a step backwards, or at least one extra step that I have to put in each action that handles POST.Ecker
I agree. But having my unit tests and the real app work the same way is worth the effort.Buckley
ARMs approach is better IMHO :)Filose
This kind of defeats the purpose of MVC.Aventurine
I agree that ARM's answer is better. Passing in a FormCollection to a controller action is undesirable, in comparison to passing a strongly typed Model/ViewModel object.Prestige
I feel funny updating this, as we're now into MVC4 (with MVC5 just down the road), and my MVC3 memory is a bit rusty.. but as you go into MVC3 and MVC4, there are new calls on the controller class called ValidateModel([model]) and TryValidateModel([model]), which can be called.Alkaloid
G
195

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));
}
Germander answered 28/9, 2010 at 19:2 Comment(6)
I agree, this should be the correct answer. As ARM says: the built-in validation should not be tested. Instead, the behaviour of your controller should be the thing that is tested. That makes the most sense.Prestige
Controller should be tested separately from model binding and validation. Follows both KISS and separation of concern. I am making a small series of articles on unit testing MVC components here timoch.com/blog/2013/06/…Shela
What should you do in order to test custom validation attributes? If those are being used, then one cannot "trust MVC's validation". How would you test (in the model tests, presumably) that the custom validation is working?Bethel
I dont agree. We still need to verify that a given model will produce the model-errors used as precondition in this test. The example code is however a perfect answer to your own defined question in 1. However it is not the answer to the initial questionPolled
This is not testing the model validation. Case in point, someone could (intentionally or accidentally) remove a data annotation in the model (maybe a merging error?) and this test will not fail.Deccan
@RosdiKasim Then you write a test that confirms that you have said attribute on the model.Germander
W
91

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.

Wooer answered 28/7, 2010 at 13:8 Comment(5)
Sorry hadn't even checked that. All our MVC projects are in 4.0Wooer
Thanks for this! A small addendum; if you have validation that isn't coupled to a specific field (i.e. you've implemented IValidatableObject), the MemberNames is empty, and the model error key should be an empty string. In the foreach you can do: var key = validationResult.MemberNames.Any() ? validationResult.MemberNames.First() : string.Empty; controller.ModelState.AddModelError(key, validationResult.ErrorMessage);Oletaoletha
Why does that need to be using Generics? This could be consumed much easier if it were defined as : void ValidateViewModel(object viewModelToValidate, Controller controller) or even better as an extension method: public static void ValidateViewModel(this Controller controller, object viewModelToValidate)Quean
This is great, but I agree with Chad just get rid of the generic syntax.Dayle
If anybody had the same problem as me with the "Validator", then use "System.ComponentModel.DataAnnotations.Validator.TryValidateObject" to make sure you use the correct Validator.Donatelli
E
8

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.

Epochmaking answered 13/8, 2009 at 3:58 Comment(2)
I like the term "ambient validation". But there must be a way to trigger this in a unit test though?Ecker
The issue though is that you're basically testing the MVC framework - not your controller. You're trying to confirm that MVC is validating your model as you expect. The only way to do that with any certainty would be to mock the entire MVC pipeline and simulate a web request. That's probably more than you really need to know. If you're just testing that the data validation on your models is setup correctly you can do that without the controller and just run the data validation manually.Epochmaking
N
2

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);
        }
Nonpros answered 18/2, 2010 at 19:50 Comment(1)
This does not work if you have got more than one validation attribute on one property. Add this line controller.ModelState.Clear(); before the code that creates ModelBindingContext and it would workLevey
F
2

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.

Flux answered 5/10, 2010 at 3:45 Comment(0)
P
1

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.

Paton answered 23/9, 2010 at 19:52 Comment(2)
Doing validation this way throws away some of the best parts of validation in MVC. I want to add validation on my model, not in the controller. If I use this solution I will end up with a lot of possible code duplicates with the accompanying nightmares.Baeza
@W.Meints: right, but the lines of code in the above example that do the validation could also be moved to a method on the Model if you prefer. The point is, doing the validation via code rather than Attributes makes it more testable. Paul explains it better above https://mcmap.net/q/262593/-unit-tests-on-mvc-validationPaton
P
1

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);
}
Prestige answered 26/5, 2012 at 16:41 Comment(1)
How we are going to test built-in validation then? Especially if we customized it with extra attributes, error messages etc.Sigrid
B
1

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.

Bacchic answered 21/3, 2013 at 20:38 Comment(0)
P
1

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();
Polled answered 1/8, 2015 at 22:4 Comment(0)
G
0

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...

Graveclothes answered 30/3, 2015 at 20:10 Comment(0)
W
0

@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);
        }
    }
Wiggle answered 9/2, 2017 at 8:42 Comment(0)
B
-4

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.

Buckley answered 13/8, 2009 at 7:32 Comment(6)
I like this approach, but it seems like a step backwards, or at least one extra step that I have to put in each action that handles POST.Ecker
I agree. But having my unit tests and the real app work the same way is worth the effort.Buckley
ARMs approach is better IMHO :)Filose
This kind of defeats the purpose of MVC.Aventurine
I agree that ARM's answer is better. Passing in a FormCollection to a controller action is undesirable, in comparison to passing a strongly typed Model/ViewModel object.Prestige
I feel funny updating this, as we're now into MVC4 (with MVC5 just down the road), and my MVC3 memory is a bit rusty.. but as you go into MVC3 and MVC4, there are new calls on the controller class called ValidateModel([model]) and TryValidateModel([model]), which can be called.Alkaloid

© 2022 - 2024 — McMap. All rights reserved.