Тестирование модуля Проверка данных ASP.NET DataAnnotations
Я использую DataAnnotations для моей проверки модели i.e.
[Required(ErrorMessage="Please enter a name")]
public string Name { get; set; }
В моем контроллере я проверяю значение ModelState. Это верно возвращает false для недопустимых данных модели, опубликованных с моего представления.
Однако при выполнении unit test моего действия с контроллером 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);
}
Нужно ли мне что-то делать, чтобы настроить проверку модели в моих тестах?
Спасибо,
Бен
Ответы
Ответ 1
Проверка будет выполняться с помощью ModelBinder
. В примере вы сами создаете ShippingDetails
, который пропустит ModelBinder
и, таким образом, будет полностью проверен. Обратите внимание на разницу между проверкой ввода и проверкой модели. Проверка входа - это убедиться, что пользователь предоставил некоторые данные, учитывая, что у него была такая возможность. Если вы предоставите форму без соответствующего поля, связанный с ней валидатор не будет вызываться.
В MVC2 произошли изменения в отношении проверки модели по сравнению с проверкой ввода, поэтому точное поведение зависит от используемой вами версии. Подробнее см. http://bradwilson.typepad.com/blog/2010/01/input-validation-vs-model-validation-in-aspnet-mvc.html в отношении MVC и MVC 2.
[EDIT] Я думаю, что самым чистым решением для этого является вызов UpdateModel
на контроллере вручную при тестировании путем предоставления пользовательского макета ValueProvider
. Это должно устранить проверку и правильно установить ModelState
.
Ответ 2
Я разместил это в своем блоге:
using System.ComponentModel.DataAnnotations;
// model class
public class Fiz
{
[Required]
public string Name { get; set; }
[Required]
[RegularExpression("[email protected]+")]
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;
}
Ответ 3
Я проходил через http://bradwilson.typepad.com/blog/2009/04/dataannotations-and-aspnet-mvc.html, в этом посте мне не нравилась идея сдачи тестов проверки в контрольном тесте и несколько вручную проверяя в каждом тесте, что если атрибут проверки существует или нет. Итак, ниже - вспомогательный метод и его использование, которое я реализовал, он работает как для EDM (у которого есть атрибуты метаданных, из-за причины, по которой мы не можем применять атрибуты для автоматически генерируемых классов EDM), так и для объектов POCO, которые имеют ValidationAttributes, применяемые к их свойствам.
Вспомогательный метод не анализирует иерархические объекты, но проверка может быть проверена на плоских отдельных объектах (тип уровня)
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
Ответ 4
Мне нравится тестировать атрибуты данных на моделях и просматривать модели вне контекста контроллера. Я сделал это, написав собственную версию 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;
}
Затем это можно легко использовать в unit test следующим образом:
// Arrange
var viewModel = new AddressViewModel();
var addressValues = new FormCollection
{
{"CustomerName", "Richard"}
};
// Act
var modelState = TryUpdateModel(viewModel, addressValues);
// Assert
Assert.False(modelState.IsValid);
Ответ 5
У меня возникла проблема, когда 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);
}
}
}