单元测试ASP.NET DataAnnotations验证


75

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

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

我是否需要做其他任何事情来设置测试中的模型验证?

谢谢,

Answers:


21

验证将由进行ModelBinder。在该示例中,您构造了ShippingDetails自己,这将跳过ModelBinder和从而完全验证。注意输入验证和模型验证之间的区别。输入验证是为了确保用户有机会提供一些数据。如果您提供的表单没有关联的字段,则不会调用关联的验证器。

MVC2在模型验证和输入验证方面进行了更改,因此确切的行为取决于您使用的版本。有关MVC和MVC 2的详细信息,请参见http://bradwilson.typepad.com/blog/2010/01/input-validation-vs-model-validation-in-aspnet-mvc.html

[编辑]我想最干净的解决方案是UpdateModel在测试时通过提供自定义模拟手动调用Controller ValueProvider。那应该触发验证并ModelState正确设置。


122

我将其发布在我的博客文章中

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

1
这是一个很好,干净且简单的实现,它使用已经为验证而编写的代码,而不是尝试重新发明轮子。
Makotosan 2012年

1
我得到的解决方案几乎与您一样(减去了从我的答案中消除代码重复的私有方法)stackoverflow.com/a/11993444/1027808
Phil Patterson

3
请注意,这不会通过对复杂属性的验证来递归
KyleMit 2015年

1
使用trueValidator.TryValidateObject(model, ctx, validationResults, true);保存了一天。我有一个必需的验证,也有一个属性的正则表达式验证。在使用之前true,即使第二次验证通过也不会通过测试。感谢您的回答。
Curiousity

“请注意,这不会通过对复杂属性的验证来递归。” ^不错,我将做两点说明。1)在这里解决:stackoverflow.com/questions/7663501/…也在这里:fluentvalidation.net/aspnet.html#asp-net-mvc-5 2)这仍然是ASP.NET MVC 5(。 NET Full / Classic)。我还没有看过.NET Core。但是,嘿,微软?这需要大修。
乔恩·戴维斯

23

我正在浏览http://bradwilson.typepad.com/blog/2009/04/dataannotations-and-aspnet-mvc.html,在这篇文章中,我不喜欢将验证测试放入控制器测试的想法,在每个测试中手动检查验证属性是否存在。因此,下面是我实现的辅助方法及其用法,它适用于EDM(具有元数据属性,因为我们不能将属性应用于自动生成的EDM类的原因)和已将ValidationAttributes应用于其属性的POCO对象。

helper方法不会解析为分层对象,但是可以在单个平面对象(类型级别)上测试验证

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

}

这是另一篇文章http://johan.driessen.se/archive/2009/11/18/testing-dataannotation-based-validation-in-asp.net-mvc.aspx,它讨论了在.Net 4中进行验证的问题,但我认为我将坚持使用在3.5和4中均有效的辅助方法


如果要测试它是正确的格式怎么办?例如,我想测试电子邮件是否遵循正则表达式。
VinnyG 2010年

Vinny,您需要实现一个继承自ValidationAttribute的新类,以放置实际的验证逻辑...在您检查有效电子邮件地址格式的情况下。
天蝎座

9

我喜欢在模型上测试数据属性,并在控制器的上下文之外查看模型。我通过编写自己的TryUpdateModel版本来做到这一点,该版本不需要控制器,可以用来填充ModelState字典。

这是我的TryUpdateModel方法(主要来自.NET MVC Controller源代码):

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

然后可以轻松地将其用于如下所示的单元测试中:

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

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

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

自定义控制器很棒!
MilesMorales

我知道这是从很久以前开始的,但是ControllerContext是什么?我找不到吗?
拉菲

@Rafi ControllerContext在程序集System.Web.Mvc中
Richard

1

我遇到一个问题,即TestsHelper大部分时间都在工作,但不适用于IValidatableObject接口定义的验证方法。CompareAttribute还给了我一些问题。这就是为什么try / catch在其中的原因。以下代码似乎可以验证所有情况:

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);
        }
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.