我正在使用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);
}
在我的测试中,我是否需要做额外的事情来设置模型验证?
谢谢,
本
答案 0 :(得分:92)
我在my blog post发布了这个:
// model class
using System.ComponentModel.DataAnnotations;
namespace MvcApplication2.Models
{
public class Fiz
{
[Required]
public string Name { get; set; }
[Required]
[RegularExpression(".+@..+")]
public string Email { get; set; }
}
}
// test class
[TestMethod]
public void EmailRequired()
{
var fiz = new Fiz
{
Name = "asdf",
Email = null
};
Assert.IsTrue(ValidateModel(fiz).Count > 0);
}
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 :(得分:23)
我正在经历http://bradwilson.typepad.com/blog/2009/04/dataannotations-and-aspnet-mvc.html,在这篇文章中,我不喜欢将验证测试放在控制器测试中,并且在每个测试中有一些手动检查,如果验证属性存在与否。因此,下面是帮助方法及其实现的用法,它适用于EDM(具有元数据属性,因为我们无法在自动生成的EDM类上应用属性)和将ValidationAttributes应用于其属性的POCO对象
辅助方法不会解析为分层对象,但验证可以在单个对象上进行测试(类型级别)
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中都有效的辅助方法
答案 2 :(得分:19)
验证将由ModelBinder
执行。在该示例中,您自己构造ShippingDetails
,这将跳过ModelBinder
,从而完全验证。请注意输入验证和模型验证之间的区别。输入验证是为了确保用户提供了一些数据,因为他有机会这样做。如果您提供没有关联字段的表单,则不会调用关联的验证程序。
MVC2在模型验证和输入验证方面有所变化,因此确切的行为取决于您使用的版本。有关MVC和MVC 2的详细信息,请参阅http://bradwilson.typepad.com/blog/2010/01/input-validation-vs-model-validation-in-aspnet-mvc.html。
[编辑]我想最干净的解决方案是在测试时通过提供自定义模拟UpdateModel
手动调用控制器上的ValueProvider
。这应该触发验证并正确设置ModelState
。
答案 3 :(得分:8)
我喜欢测试模型上的数据属性,并在控制器的上下文之外查看模型。我通过编写自己的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);
答案 4 :(得分: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);
}
}
}