Модульное тестирование, проверка аннотаций данных ASP.NET

Я использую 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);
}

Нужно ли мне делать что-то еще, чтобы настроить проверку модели в моих тестах?

Спасибо,

Бен


person Ben Foster    schedule 30.01.2010    source источник


Ответы (5)


Проверка будет выполнена ModelBinder. В этом примере вы сами создаете ShippingDetails, что полностью пропустит ModelBinder и, следовательно, проверку. Обратите внимание на разницу между проверкой ввода и проверкой модели. Проверка ввода заключается в том, чтобы убедиться, что пользователь предоставил некоторые данные, если у него была возможность это сделать. Если вы предоставите форму без связанного поля, связанный валидатор не будет вызван.

В MVC2 были внесены изменения в отношении проверки модели и проверки ввода, поэтому точное поведение зависит от версии, которую вы используете. См. http://bradwilson.typepad.com/blog/2010/01/input-validation-vs-model-validation-in-aspnet-mvc.html для получения подробной информации о MVC и MVC 2.

[РЕДАКТИРОВАТЬ] Я полагаю, что самым чистым решением этой проблемы является вызов UpdateModel на Контроллере вручную при тестировании, предоставив собственный макет ValueProvider. Это должно запустить проверку и правильно установить ModelState.

person mnemosyn    schedule 30.01.2010

Я разместил это в мое сообщение в блоге:

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;
}
person Jon Davis    schedule 02.12.2010
comment
Это была красивая, чистая и простая реализация, в которой вместо попытки заново изобрести колесо используется код, который уже написан для проверки. - person Makotosan; 29.06.2012
comment
Я пришел к тому же решению, что и вы (за исключением частного метода, который исключает дублирование кода из моего ответа) stackoverflow.com/a/ 11993444/1027808 - person Phil Patterson; 25.09.2012
comment
Обратите внимание, что это не будет повторяться через проверки сложных свойств. - person KyleMit; 10.02.2015
comment
Использование true в Validator.TryValidateObject(model, ctx, validationResults, true); спасло положение. У меня была необходимая проверка, а также проверка регулярного выражения для одного свойства. До использования true тесты проходили, даже если вторая проверка прошла, хотя ее и не должно быть. Спасибо за этот ответ. - person Curiousity; 08.08.2017
comment
Обратите внимание, что это не будет повторяться при проверке сложных свойств ^ Хороший улов, и я сделаю два примечания. 1) Это рассматривается здесь: stackoverflow.com/questions/7663501/, а также здесь: fluentvalidation. net / aspnet.html # asp-net-mvc-5 2) Это все еще проблема в ASP.NET MVC 5 (.NET Full / Classic). Я еще не смотрел .NET Core. Но что за хрень, Microsoft? Это требует капитального ремонта. - person Jon Davis; 31.07.2018
comment
Отредактировал образец для проверки не только того, что объект проверки вернулся, но и того, что хотя бы один объект проверки соответствует правилу, необходимому для электронной почты. - person Jon Davis; 17.10.2019

Я просматривал 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

person scorpio    schedule 03.02.2010
comment
Как вы это делаете, если хотите проверить, что это правильный формат? В качестве примера я хотел бы проверить, соответствует ли электронное письмо регулярному выражению. - person VinnyG; 26.11.2010
comment
Винни, вам нужно будет реализовать новый класс, унаследованный от ValidationAttribute, чтобы поставить фактическую логику проверки ... в вашем случае проверка правильного формата адреса электронной почты. - person scorpio; 03.12.2010

Мне нравится тестировать атрибуты данных на моих моделях и просматривать модели вне контекста контроллера. Я сделал это, написав свою собственную версию 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);
person Richard Garside    schedule 26.02.2014
comment
Пользовательский контроллер был великолепен! - person MilesMorales; 31.01.2017
comment
Я знаю, что это было давным-давно, но что такое ControllerContext? Я не могу его найти? - person Rafi; 04.12.2017
comment
@Rafi ControllerContext находится в сборке System.Web.Mvc - person Richard Garside; 06.12.2017

У меня возникла проблема, когда TestsHelper работал большую часть времени, но не для методов проверки, определенных интерфейсом IValidatableObject. CompareAttribute также доставил мне некоторые проблемы. Вот почему попытка / уловка здесь. Следующий код, кажется, проверяет все случаи:

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);
        }
    }
}
person Vance Kessler    schedule 01.02.2012