Wednesday, February 23, 2011

How to unit test your DataAnnotations attributes

Earlier on the Twitter, I helped a pal solve a problem they were having with unit testing their DataAnnotations implementation.  He compelled me to blog my solution, and I agree -- what a great subject!

So let's take a pretty common use-case: validating an email address.  Email addresses are a slippery fish, so I'll make a DataAnnotations validator to make sure that an input's value looks like it's probably in a valid email address format.

I start with a regex validator that I think I pulled off of Scott Guthrie's blog:

public class RegexAttribute: ValidationAttribute
{
  public string Pattern { get; set; }
  public RegexOptions Options { get; set; }

  public RegexAttribute(string pattern)
      : this(pattern, RegexOptions.None) {}

  public RegexAttribute(string pattern, RegexOptions options)
  {
    Pattern = pattern;
    Options = options;
  }

  public override bool IsValid(object value)
  {
    //null or empty is valid, let Required handle null/empty
    var str = (string)value;
    return string.IsNullOrEmpty(str) || 
      new Regex(Pattern, Options).IsMatch(str);
  }

}


Simple enough, let's extend it and make an Email validator:

public class EmailAttribute : RegexAttribute
{
  public EmailAttribute() : 
    base(@"^(([^<>()[\]\\.,;:\s@\""]+(\.[^<>()[\]\\.,;:\s@\""]+)*)|(\"".+\""))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$", 
    RegexOptions.None) {}
}


Pretty simple.  I bet I could get away with putting it all on one line since no one actually reads regular expressions.

Now here's the magic part.  I'm going to use Reflection to grab all the attributes that are of the type ValidationAttribute and see if those attributes are valid.  If they aren't, I'll make a list of ErrorInfo objects.  I call it my ValidationBuddy.

public static class ValidationBuddy
{
  public static IList<ErrorInfo> GetTypeErrors(object instance)
  {
    var attributes = TypeDescriptor.GetAttributes(instance)
      .OfType<ValidationAttribute>();
      return (from attribute in attributes
              let result = attribute.GetValidationResult(instance, 
          new ValidationContext(instance, null, null))
              where result != null
              select new ErrorInfo(instance.GetType().ToString(), 
          attribute.FormatErrorMessage(string.Empty), 
          instance))
      .ToList();
  }

  public static IList<ErrorInfo> GetPropertyErrors(object instance)
  {
    return (from prop in TypeDescriptor.GetProperties(instance)
      .Cast<PropertyDescriptor>()
        from attribute in prop.Attributes
          .OfType<ValidationAttribute>()
        where !attribute.IsValid(prop.GetValue(instance))
        select new ErrorInfo(prop.Name, 
          attribute.FormatErrorMessage(prop.DisplayName), instance))
         .ToList();
  }

  public static IList<ErrorInfo> GetErrors(object instance)
  {
    var list = GetPropertyErrors(instance);

    foreach (var error in GetTypeErrors(instance))
      list.Add(error);

    return list;
  }
}


The ErrorInfo class is pretty simple, too:

public class ErrorInfo
{
  public ErrorInfo(){}
  public ErrorInfo(string propertyName, string errorMessage, object instance)
  {
    PropertyName = propertyName;
    ErrorMessage = errorMessage;
    Instance = instance;
  }

  public string PropertyName { get; set; }
  public string ErrorMessage { get; set; }
  public object Instance { get; set; }
}


And finally, a single unit test:

[TestMethod]
public void TestEmailValidator05()
{
  // Arrange
  var model = GetModel();
  model.EmailAddress = "uncommonTLD@domain.travel";

  // Act
  var errors = ValidationBuddy.GetErrors(model);

  // Assert
  Assert.IsTrue(!errors.Any());
}


In my actual test class, I have quite a few more tests, but this should get you started in writing your own DataAnnotations unit tests.

2 comments:

Ygor said...

How would one test with types "DateTime" and "int"?

For example:

[Required(ErrorMessage = "The date field is required.")]
[DataType(DataType.Date, ErrorMessage = "Invalid date.")]
public DateTime Date { get; set; }

Thanks a lot!

TiMoch said...

JM, nice post, I like the ValidationBuddy helper.

Ygor, have a look here http://timoch.com/blog/2013/06/unit-testing-model-validation-with-mvcs-dataannotations/

It's a simple solution to test your DataAnnotations validation rules in separation.