85 votes

Unité de test de validation ASP.NET DataAnnotations

J'utilise DataAnnotations pour la validation de mon modèle, c'est-à-dire

     [Required(ErrorMessage="Please enter a name")]
    public string Name { get; set; }
 

Dans mon contrôleur, je vérifie la valeur de ModelState. Cela renvoie correctement la valeur false pour les données de modèle non valides publiées de mon point de vue.

Cependant, lors de l'exécution du test unitaire de l'action de mon contrôleur, ModelState renvoie toujours 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);
    }
 

Dois-je faire quelque chose de plus pour configurer la validation du modèle dans mes tests?

Merci,

Ben

149voto

stimpy77 Points 2791

J'ai posté ceci dans mon blog :

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

23voto

scorpio Points 373

J'ai été en passant par http://bradwilson.typepad.com/blog/2009/04/dataannotations-and-aspnet-mvc.htmldans ce post, je n'aimais pas l'idée de mettre les tests de validation dans le contrôleur de test et un peu de contrôle manuel dans chaque test si la validation de l'attribut existe ou pas. Alors, ci-dessous est la méthode d'assistance et c'est l'utilisation qui j'ai mis en place, il fonctionne pour EDM (qui a les attributs de métadonnées, en raison de la raison pour laquelle nous ne pouvons pas appliquer les attributs générés automatiquement, EDM classes) et des objets POCO qui ont ValidationAttributes appliquées à leurs propriétés.

La méthode d'assistance de ne pas analyser hiérarchique des objets, mais la validation peut être testé sur le plat des objets individuels(au niveau du Type)

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

}

c'est un autre post http://johan.driessen.se/archive/2009/11/18/testing-dataannotation-based-validation-in-asp.net-mvc.aspx qui parle de validation en .Net 4, mais je pense que je vais en tenir à ma méthode d'assistance qui est valable dans les deux 3,5 et 4

23voto

mnemosyn Points 17378

La Validation sera effectuée par l' ModelBinder. Dans l'exemple, vous construisez l' ShippingDetails vous-même, qui va sauter dans l' ModelBinder et donc, la validation entièrement. Notez la différence entre la validation de la saisie et de la validation du modèle. Validation de l'entrée est de s'assurer que l'utilisateur a fourni certaines données, étant donné qu'il avait la chance de le faire. Si vous fournir un formulaire sans que le champ associé, l'associé du programme de validation de ne pas être invoquée.

Il y a eu des changements dans MVC2 sur la validation du modèle vs validation de l'entrée, de sorte que le comportement exact dépend de la version que vous utilisez. Voir http://bradwilson.typepad.com/blog/2010/01/input-validation-vs-model-validation-in-aspnet-mvc.html pour plus de détails sur ce sujet à la fois MVC et MVC 2.

[EDIT] je suppose que la solution la plus propre à cet appel UpdateModel sur le Contrôleur manuellement lors de l'essai par la fourniture d'une maquette personnalisée ValueProvider. Qui doit se déclencher de validation et de définir l' ModelState correctement.

10voto

Richard Points 14490

J'aime tester les attributs de données sur mes modèles et voir des modèles en dehors du contexte du contrôleur. Pour ce faire, j'ai écrit ma propre version de TryUpdateModel ne nécessitant pas de contrôleur et pouvant être utilisée pour renseigner un dictionnaire ModelState.

Voici ma méthode TryUpdateModel (principalement tirée du code source du contrôleur .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;
}
 

Ceci peut alors être facilement utilisé dans un test unitaire comme ceci:

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

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

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

1voto

Vance Kessler Points 111

J'ai eu un problème où TestsHelper travaillait la plupart du temps, mais pas pour les méthodes de validation définies par l'interface IValidatableObject. Le CompareAttribute m'a également donné quelques problèmes. C'est pourquoi le try / catch est là. Le code suivant semble valider tous les cas:

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X