web-dev-qa-db-fra.com

Test unitaire de validation DataAnnotations ASP.NET

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 false pour les données de modèle non valides publiées à partir de ma 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

65
Ben Foster

La validation sera effectuée par le ModelBinder. Dans l'exemple, vous construisez vous-même le ShippingDetails, ce qui sautera le ModelBinder et donc la validation entièrement. Notez la différence entre la validation d'entrée et la validation de modèle. La validation des entrées consiste à s'assurer que l'utilisateur a fourni certaines données, étant donné qu'il en a eu la possibilité. Si vous fournissez un formulaire sans le champ associé, le validateur associé ne sera pas appelé.

Il y a eu des changements dans MVC2 sur la validation du modèle par rapport à la validation d'entrée, donc 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 à ce sujet concernant MVC et MVC 2.

[EDIT] Je suppose que la solution la plus propre à cela est d'appeler UpdateModel sur le contrôleur manuellement lors des tests en fournissant une maquette personnalisée ValueProvider. Cela devrait déclencher la validation et définir correctement le ModelState.

19
mnemosyn

J'ai posté ceci dans mon article de 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;
}
99
Jon Davis

Je traversais http://bradwilson.typepad.com/blog/2009/04/dataannotations-and-aspnet-mvc.html , dans ce post, je n'aimais pas l'idée de mettre le tests de validation dans le test du contrôleur et vérification quelque peu manuelle dans chaque test que si l'attribut de validation existe ou non. Donc, ci-dessous est la méthode d'assistance et son utilisation que j'ai implémentée, elle fonctionne à la fois pour EDM (qui a des attributs de métadonnées, car nous ne pouvons pas appliquer d'attributs sur les classes EDM générées automatiquement) et les objets POCO qui ont des ValidationAttributes appliqués à leurs propriétés .

La méthode d'assistance n'analyse pas les objets hiérarchiques, mais la validation peut être testée sur des objets individuels plats (niveau 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 article http://johan.driessen.se/archive/2009/11/18/testing-dataannotation-based-validation-in-asp.net-mvc.aspx qui parle de validation dans .Net 4, mais je pense que je vais m'en tenir à ma méthode d'assistance qui est valide à la fois en 3.5 et 4

23
scorpio

J'aime tester les attributs de données sur mes modèles et voir des modèles en dehors du contexte du contrôleur. J'ai fait cela en écrivant ma propre version de TryUpdateModel qui n'a pas besoin d'un contrôleur et peut être utilisée pour remplir 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;
}

Cela peut ensuite être facilement utilisé dans un test unitaire comme celui-ci:

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

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

// Assert
Assert.False(modelState.IsValid);
8
Richard Garside

J'ai eu un problème où TestsHelper fonctionnait la plupart du temps mais pas pour les méthodes de validation définies par l'interface IValidatableObject. Le CompareAttribute m'a également posé 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);
        }
    }
}
1
Vance Kessler