【问题标题】:Unit Testing ASP.NET DataAnnotations validation单元测试 ASP.NET DataAnnotations 验证
【发布时间】:2011-01-11 04:30:44
【问题描述】:

我正在使用 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);
}

我需要做任何额外的事情来在我的测试中设置模型验证吗?

谢谢,

本

【问题讨论】:

    标签: asp.net asp.net-mvc data-annotations


    【解决方案1】:

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

    【讨论】:

    • 这是一个不错的、干净且简单的实现,它使用已经为验证而编写的代码,而不是试图重新发明轮子。
    • 我得到的解决方案与您的解决方案几乎相同(减去从我的答案中消除代码重复的私有方法)stackoverflow.com/a/11993444/1027808
    • 请注意,这不会通过复杂属性的验证来递归
    • 在Validator.TryValidateObject(model, ctx, validationResults, true); 中使用true 拯救了这一天。我对单个属性进行了必需的验证和正则表达式验证。在使用true 之前,即使第二次验证也通过了测试,尽管它不应该通过。谢谢你的回答。
    • “请注意,这不会通过对复杂属性的验证进行递归” ^ 很好,对此我将做两点说明。 1) 这在此处解决:stackoverflow.com/questions/7663501/… 和此处:fluentvalidation.net/aspnet.html#asp-net-mvc-5 2) 这仍然是 ASP.NET MVC 5 (.NET Full/Classic) 中的一个问题。我还没有看过 .NET Core。但是,嘿,微软?这需要大修。
    【解决方案2】:

    我喜欢在我的模型上测试数据属性,并在控制器上下文之外查看模型。为此,我编写了自己的 TryUpdateModel 版本,不需要控制器,可用于填充 ModelState 字典。

    这是我的 TryUpdateModel 方法(主要取自 .NET MVC 控制器源代码):

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

    【讨论】:

    • 自定义控制器很棒!
    • 我知道这是很久以前的了,但 ControllerContext 是什么?没找到?
    • @Rafi ControllerContext 在程序集 System.Web.Mvc 中
    【解决方案3】:

    我遇到了一个问题,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);
            }
        }
    }
    

    【讨论】:

      【解决方案4】:

      我正在经历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 中都有效的辅助方法

      【讨论】:

      • 如果你想测试它的格式是否正确,你会怎么做?例如,我想测试一下电子邮件是否遵循正则表达式。
      • Vinny,您需要实现一个继承自 ValidationAttribute 的新类,以放置实际的验证逻辑...在您的情况下检查有效的电子邮件地址格式。
      【解决方案5】:

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

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-11-13
        • 2015-10-01
        • 1970-01-01
        • 2017-05-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-03-26
        相关资源
        最近更新 更多