views:

1095

answers:

7

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?

+3  A: 

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.

Paul Alexander
I like the term "ambient validation". But there must be a way to trigger this in a unit test though?
mgroves
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.
Paul Alexander
Both answers so far have been great, I wish I could give you both a checkmark.
mgroves
+2  A: 

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.

Maurice
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.
mgroves
I agree. But having my unit tests and the real app work the same way is worth the effort.
Maurice
+2  A: 

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);
        }
gustavogb
+6  A: 

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:

private static void ValidateViewModel<VM, C>(VM viewModelToValidate, C controller) where C:Controller
{
    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.First(), validationResult.ErrorMessage);
    }
}

So far this has worked really well for us.

Giles
Very cool, I can definitely use this :)
mgroves
Nice, but only works in .NET 4 apparently, not 3.5
codeulike
Sorry hadn't even checked that. All our MVC projects are in 4.0
Giles
+1  A: 

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.

codeulike
+2  A: 

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));
}
ARM
+1  A: 

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.

Darren