单元测试 ASP.NET 数据注释验证

我正在使用 DataAnnotions 进行模型验证,即。

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

在我的控制器中,我正在检查 ModelState 的值。对于从我的视图发布的无效模型数据,这正确地返回 false。

但是,在执行控制器操作的单元测试时,ModelState 总是返回 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);
}

我是否需要做一些额外的工作来设置测试中的模型验证?

38762 次浏览

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.

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

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;
}

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);
}
}
}

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);