Coding and Dismantling Stuff

Don't thank me, it's what I do.

About the author

Russell is a .Net developer based in Lancashire in the UK.  His day job is as a C# developer for the UK's largest online white-goods retailer, DRL Limited.

His weekend job entails alternately demolishing and constructing various bits of his home, much to the distress of his fiance Kelly, 3-year-old daughter Amelie, and menagerie of pets.

TextBox

  1. Fix dodgy keywords Google is scraping from my blog
  2. Complete migration of NHaml from Google Code to GitHub
  3. ReTelnet Mock Telnet Server à la Jetty
  4. Learn to use Git
  5. Complete beta release FHEMDotNet
  6. Publish FHEMDotNet on Google Code
  7. Learn NancyFX library
  8. Pull RussPAll/NHaml into NHaml/NHaml
  9. Open Source Blackberry Twitter app
  10. Other stuff

How To Test Your MVC3 Model Validation and Controller Logic

Hi all,

Today, I've come across some code that's testing an MVC controller to ensure that the controller responds correctly if the model passed in is invalid. This sounds a good plan - except that you need to be mindful that your controller doesn't validate your model! Your view model is validated by a separate class, the ModelBinder, that fires up just before your controller is executed. This ModelBinder is built into the MVC engine, you can bet that it's been tested a zillion times by a zillion other devs, so you definitely don't want to find some way to test this ModelBinder directly.

So what to do? Let's take a look at the original test, exactly what code we've written, and so what we want to test.

The Original Test

To abridge things somewhat, let's assume our original model and controller looks something like the following (The usual caveat applies, none of my code is guaranteed to run if you copy-and-paste!):

public class LogOnViewModel
{
   [Required(ErrorMessage = "* Required")]
   public string Username { get; set; }
   
   [Required(ErrorMessage = "* Required")]
   public string Hash { get; set; }
}

public class AccountController : Controller
{
   [HttpPost]
   public ViewResult LogOn(LogOnViewModel model)
   {
      if (!ModelState.IsValid)
         return View(model);
      return Redirect(Url.Action("Home", "Index"));
   }
}

And the test for the above looks like:

[TestFixture]   
public class AccountController_Tests
{
   [Test]
   public void LogOn_InvalidViewModel_ReturnsCorrectView()
   {
      var model = new LogOnViewModel
                  {
                     Username = surname,
                     Hash = null
                  };

      var result = _accountController.LogOn(model);

      Assert.IsInstanceOf<ViewResult>(result);
   }
}

If you try spinning up the above code, you'll find the test doesn't work. You're passing in an invalid model, but you're still getting a Redirect out instead of the expected ViewResult. Remember, this is because you've directly called your controller in the test and so bypassed the ModelBinder. How do we get the above test to work?

Testing Logic In our Views

The test above is really saying, "Given an invalid model, my AccountController does the right stuff". Since we don't want to test ModelBinding directly, the simplest way to do this is to use the ModelState property on your controller, as follows:

[TestFixture]   
public class AccountController_Tests
{
   [Test]
   public void LogOn_InvalidViewModel_ReturnsCorrectView()
   {
      _accountController.ModelState.AddModelError("", "");
      var result = _accountController.LogOn(new LogOnViewModel());
      Assert.IsInstanceOf<ViewResult>(result);
   }
}

Much cleaner!

Annotations On our Model - Option 1

We've now tested that when the controller thinks a model is invalid, it behaves correctly. Next up, we want to test our model to confirm that the data annotations we've put onto our model are correct. There are two ways you can do this. You use code similar to the following (from this StackOverflow answer) to test for the presence of the attributes:

[Test]
public void LogOnViewModel_Username_PropertyIsRequired()
{
   // Arrange
   PropertyInfo propertyInfo = typeof(LogOnViewModel)
      .GetProperty("Username");

   // Act
   RequiredAttribute attribute = propertyInfo
      .GetCustomAttributes(typeof(RequiredAttribute), true)
      .Cast<RequiredAttribute>()
      .FirstOrDefault();

   // Assert
   Assert.NotNull(attribute);
}

Of course you'd refactor a lot of this out into helper methods, maybe using a extension methods, to give you code more like:

[Test]
public void LogOnViewModel_Username_PropertyIsRequired()
{
   // Arrange
   PropertyInfo propertyInfo = typeof(LogOnViewModel)
      .GetProperty("Username");

   // Assert
   Assert.IsTrue(propertyInfo.HasRequiredAttribute());
}

private static class DataAnnotationsTestHelper
{
   public static bool HasRequiredAttribute(PropertyInfo property)
   {
      var attribute = propertyInfo
         .GetCustomAttributes(typeof(RequiredAttribute), true)
         .Cast<RequiredAttribute>()
         .FirstOrDefault();

      return (attribute != null);
   }
}

Annotations On our Model - Option 2

I'm not a huge fan of the above option. Even though with the DataAnnotationsTestHelper class we can make our tests nice and concise, personally I'd rather test that the annotations correctly fail validation when they're run into the MVC validator.

But wait - isn't that against the whole idea of unit tests? Aren't we now testing two things, the ViewModel annotations and the MVC validator? In my eyes, not really. The MVC engine's been tested to death by sheer use, and even if your annotations are correct and the MVC engine burps on them due to a bug, are you personally gonna fix that MVC bug? Probably not, so why not catch that in your unit tests.

This is how I'm now validating my model annotations.

public void Validation_UsernameField_IsRequired()
{
   var model = new LogOnViewModel
   {
      Username = null,
      Hash = "Hash"      
   };

   var errors = GetValidationErrors(model);
   Assert.That(errors[0].ErrorMessage, Is.EqualTo("* Required"));
}

private IList<ValidationResult> GetValidationErrors(LogOnViewModel model)
{
   var validationContext = new ValidationContext(model, null, null);
   var validationResults = new List<ValidationResult>();
   Validator.TryValidateObject(model, validationContext, validationResults);
   return validationResults;
}

Each test is a little more verbose, but I'd feel a lot more confident seeing these tests pass than the first option of testing attributes directly.


Permalink | Comments (1)

Comments (1) -

Victor Honduras

18 January 2013 17:09

Victor

If your testing more than 1 data annotations attribute you need to add to Validator.TryValidateObject(model, validationContext, validationResults); the boolean true so it tests all validation attributes in the model.

Validator.TryValidateObject(model, validationContext, validationResults, true);

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading