I love how testable ASP.NET MVC is, I also love MVC2's model validation. However when trying to unit test a controller method that used the ModelState, I quickly learned that the ModelState is not populated when just newing up a Controller and calling one of its public methods. As usual, I think this is best narrated by example:
Example Model and Controller
public class PersonModel
{
[Required]
public string Name { get; set; }
}
public class PersonController : Controller
{
[AcceptVerbs(HttpVerbs.Get)]
public ViewResult Register(Person person)
{
return View(new PersonModel());
}
[AcceptVerbs(HttpVerbs.Post)]
public ViewResult Register(Person person)
{
if (!ModelState.IsValid)
return View(model);
PersonService.Register(person);
return View("success");
}
}
Example of the Problem
[Test]
public void RegisterTest()
{
var model = new PersonModel { Name = String.Empty }; // This is model is invalid.
var controller = new PersonController();
var result = controller.Register(model);
// This fails because the ModelState was valid, although the passed in model was not.
Assert.AreNotEqual("success", result.ViewName);
}
Solution
Other solutions I have come across were adding the errors to the model state manually, or mocking the ControllerContext as to enable the Controller's private ValidateModel method. I didn't like the former because it felt like I wasn't actually testing the model validation, and I didn't like the latter because it seemed like a lot of work to both mocking things and then still have to manually expose a private method.
My solution is (I feel) pretty simple: Add an extension method to the ModelStateDictionary that allows you to pass in a model, and it will then validate that model and add it's errors to the dictionary.
public static void AddValidationErrors(this ModelStateDictionary modelState, object model)
{
var context = new ValidationContext(model, null, null);
var results = new List<ValidationResult>();
Validator.TryValidateObject(model, context, results, true);
foreach (var result in results)
{
var name = result.MemberNames.First();
modelState.AddModelError(name, result.ErrorMessage);
}
}
Example of the Solution
[Test]
public void RegisterTest()
{
var model = new PersonModel { Name = String.Empty }; // This is model is invalid.
var controller = new PersonController();
// This populates the ModelState errors, causing the Register method to fail and the unit test to pass.
controller.ModelState.AddValidationErrors(model);
var result = controller.Register(model);
Assert.AreNotEqual("success", result.ViewName);
}
No comments:
Post a Comment