Unit Testing ASP.NET DataAnnotations validation

asp.netasp.net MvcData Annotations

asp.net Problem Overview


I am using DataAnnotations for my model validation i.e.

[Required(ErrorMessage="Please enter a name")]
public string Name { get; set; }

In my controller, I am checking the value of ModelState. This is correctly returning false for invalid model data posted from my view.

However, when executing the unit test of my controller action, ModelState always returns true:

[TestMethod]
public void Submitting_Empty_Shipping_Details_Displays_Default_View_With_Error()
{
	// Arrange
	CartController controller = new CartController(null, null);
	Cart cart = new Cart();
	cart.AddItem(new Product(), 1);

	// Act
	var result = controller.CheckOut(cart, new ShippingDetails() { Name = "" });

	// Assert
	Assert.IsTrue(string.IsNullOrEmpty(result.ViewName));
	Assert.IsFalse(result.ViewData.ModelState.IsValid);
}

Do I need to do anything extra to set up the model validation in my tests?

asp.net Solutions


Solution 1 - asp.net

I posted this in my blog post:

using System.ComponentModel.DataAnnotations;

// model class
public class Fiz
{
    [Required]
    public string Name { get; set; }

    [Required]
    [RegularExpression(".+@..+")]
    public string Email { get; set; }
}

// in test class
[TestMethod]
public void EmailRequired()
{
    var fiz = new Fiz 
        {
            Name = "asdf",
            Email = null
        };
    Assert.IsTrue(ValidateModel(fiz).Any(
        v => v.MemberNames.Contains("Email") && 
             v.ErrorMessage.Contains("required")));
}

private IList<ValidationResult> ValidateModel(object model)
{
    var validationResults = new List<ValidationResult>();
    var ctx = new ValidationContext(model, null, null);
    Validator.TryValidateObject(model, ctx, validationResults, true);
    return validationResults;
}

Solution 2 - asp.net

I was going through http://bradwilson.typepad.com/blog/2009/04/dataannotations-and-aspnet-mvc.html, in this post I didn't like the idea of putting the validation tests in controller test and somewhat manual checking in each test that if the validation attribute exists or not. So, below is the helper method and it's usage which I implemented, it works for both EDM (which has metadata attributes, because of the reason we can not apply attributes on auto generated EDM classes) and POCO objects which have ValidationAttributes applied to their properties.

The helper method does not parse into hierarchical objects, but validation can be tested on flat individual objects(Type-level)

class TestsHelper
{

    internal static void ValidateObject<T>(T obj)
    {
        var type = typeof(T);
        var meta = type.GetCustomAttributes(false).OfType<MetadataTypeAttribute>().FirstOrDefault();
        if (meta != null)
        {
            type = meta.MetadataClassType;
        }
        var propertyInfo = type.GetProperties();
        foreach (var info in propertyInfo)
        {
            var attributes = info.GetCustomAttributes(false).OfType<ValidationAttribute>();
            foreach (var attribute in attributes)
            {
                var objPropInfo = obj.GetType().GetProperty(info.Name);
                attribute.Validate(objPropInfo.GetValue(obj, null), info.Name);
            }
        }
    }
}

 /// <summary>
/// Link EDM class with meta data class
/// </summary>
[MetadataType(typeof(ServiceMetadata))]
public partial class Service
{
}

/// <summary>
/// Meta data class to hold validation attributes for each property
/// </summary>
public class ServiceMetadata
{
    /// <summary>
    /// Name 
    /// </summary>
    [Required]
    [StringLength(1000)]
    public object Name { get; set; }

    /// <summary>
    /// Description
    /// </summary>
    [Required]
    [StringLength(2000)]
    public object Description { get; set; }
}


[TestFixture]
public class ServiceModelTests 
{
    [Test]
    [ExpectedException(typeof(ValidationException), ExpectedMessage = "The Name field is required.")]
    public void Name_Not_Present()
    {
        var serv = new Service{Name ="", Description="Test"};
        TestsHelper.ValidateObject(serv);
    }
    
    [Test]
    [ExpectedException(typeof(ValidationException), ExpectedMessage = "The Description field is required.")]
    public void Description_Not_Present()
    {
        var serv = new Service { Name = "Test", Description = string.Empty};
        TestsHelper.ValidateObject(serv);
    }
   
}

this is another post http://johan.driessen.se/archive/2009/11/18/testing-dataannotation-based-validation-in-asp.net-mvc.aspx which talks about validating in .Net 4, but i think i am going to stick to my helper method which is valid in both 3.5 and 4

Solution 3 - asp.net

Validation will be performed by the ModelBinder. In the example, you construct the ShippingDetails yourself, which will skip the ModelBinder and thus, validation entirely. Note the difference between input validation and model validation. Input validation is to make sure the user provided some data, given he had the chance to do so. If you provide a form without the associated field, the associated validator won't be invoked.

There have been changes in MVC2 on model validation vs. input validation, so the exact behaviour depends on the version you are using. See http://bradwilson.typepad.com/blog/2010/01/input-validation-vs-model-validation-in-aspnet-mvc.html for details on this regarding both MVC and MVC 2.

[EDIT] I guess the cleanest solution to this is to call UpdateModel on the Controller manually when testing by providing a custom mock ValueProvider. That should fire validation and set the ModelState correctly.

Solution 4 - asp.net

I like to test the data attributes on my models and view models outside the context of the controller. I've done this by writing my own version of TryUpdateModel that doesn't need a controller and can be used to populate a ModelState dictionary.

Here is my TryUpdateModel method (mostly taken from the .NET MVC Controller source code):

private static ModelStateDictionary TryUpdateModel<TModel>(TModel model,
        IValueProvider valueProvider) where TModel : class
{
    var modelState = new ModelStateDictionary();
    var controllerContext = new ControllerContext();
		
    var binder = ModelBinders.Binders.GetBinder(typeof(TModel));
    var bindingContext = new ModelBindingContext()
    {
        ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(
            () => model, typeof(TModel)),
        ModelState = modelState,
        ValueProvider = valueProvider
    };
    binder.BindModel(controllerContext, bindingContext);
    return modelState;
}

This can then be easily used in a unit test like this:

// Arrange
var viewModel = new AddressViewModel();
var addressValues = new FormCollection
{
    {"CustomerName", "Richard"}
};

// Act
var modelState = TryUpdateModel(viewModel, addressValues);

// Assert
Assert.False(modelState.IsValid);

Solution 5 - asp.net

I had an issue where TestsHelper worked most of the time but not for validation methods defined by the IValidatableObject interface. The CompareAttribute also gave me some problems. That is why the try/catch is in there. The following code seems to validate all cases:

public static void ValidateUsingReflection<T>(T obj, Controller controller)
{
    ValidationContext validationContext = new ValidationContext(obj, null, null);
    Type type = typeof(T);
    MetadataTypeAttribute meta = type.GetCustomAttributes(false).OfType<MetadataTypeAttribute>().FirstOrDefault();
    if (meta != null)
    {
        type = meta.MetadataClassType;
    }
    PropertyInfo[] propertyInfo = type.GetProperties();
    foreach (PropertyInfo info in propertyInfo)
    {
        IEnumerable<ValidationAttribute> attributes = info.GetCustomAttributes(false).OfType<ValidationAttribute>();
        foreach (ValidationAttribute attribute in attributes)
        {
            PropertyInfo objPropInfo = obj.GetType().GetProperty(info.Name);
            try
            {
                validationContext.DisplayName = info.Name;
                attribute.Validate(objPropInfo.GetValue(obj, null), validationContext);
            }
            catch (Exception ex)
            {
                controller.ModelState.AddModelError(info.Name, ex.Message);
            }
        }
    }
    IValidatableObject valObj = obj as IValidatableObject;
    if (null != valObj)
    {
        IEnumerable<ValidationResult> results = valObj.Validate(validationContext);
        foreach (ValidationResult result in results)
        {
            string key = result.MemberNames.FirstOrDefault() ?? string.Empty;
            controller.ModelState.AddModelError(key, result.ErrorMessage);
        }
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBen FosterView Question on Stackoverflow
Solution 1 - asp.netJon DavisView Answer on Stackoverflow
Solution 2 - asp.netscorpioView Answer on Stackoverflow
Solution 3 - asp.netmnemosynView Answer on Stackoverflow
Solution 4 - asp.netRichard GarsideView Answer on Stackoverflow
Solution 5 - asp.netVance KesslerView Answer on Stackoverflow