web-dev-qa-db-fra.com

Tests unitaires sur la validation MVC

Comment puis-je tester que mon action de contrôleur place les erreurs correctes dans ModelState lors de la validation d'une entité, lorsque j'utilise la validation DataAnnotation dans MVC 2 Preview 1?

Du code pour illustrer. Tout d'abord, l'action:

    [HttpPost]
    public ActionResult Index(BlogPost b)
    {
        if(ModelState.IsValid)
        {
            _blogService.Insert(b);
            return(View("Success", b));
        }
        return View(b);
    }

Et voici un test unitaire défaillant qui, je pense, devrait réussir mais ne l'est pas (en utilisant MbUnit & Moq):

[Test]
public void When_processing_invalid_post_HomeControllerModelState_should_have_at_least_one_error()
{
    // arrange
    var mockRepository = new Mock<IBlogPostSVC>();
    var homeController = new HomeController(mockRepository.Object);

    // act
    var p = new BlogPost { Title = "test" };            // date and content should be required
    homeController.Index(p);

    // assert
    Assert.IsTrue(!homeController.ModelState.IsValid);
}

Je suppose qu'en plus de cette question, devrait Je teste la validation, et dois-je la tester de cette façon?

75
Matthew Groves

Je déteste nécro un ancien post, mais je pensais ajouter mes propres pensées (car je viens d'avoir ce problème et j'ai parcouru ce post en cherchant la réponse).

  1. Ne testez pas la validation dans vos tests de contrôleur. Soit vous faites confiance à la validation de MVC, soit vous écrivez la vôtre (c'est-à-dire ne testez pas le code des autres, testez votre code)
  2. Si vous voulez tester la validation fait ce que vous attendez, testez-le dans vos tests de modèle (je le fais pour quelques-unes de mes validations regex plus complexes).

Ce que vous voulez vraiment tester ici, c'est que votre contrôleur fait ce que vous attendez de lui lorsque la validation échoue. C'est votre code et vos attentes. Le tester est facile une fois que vous réalisez que c'est tout ce que vous voulez tester:

[test]
public void TestInvalidPostBehavior()
{
    // arrange
    var mockRepository = new Mock<IBlogPostSVC>();
    var homeController = new HomeController(mockRepository.Object);
    var p = new BlogPost();

    homeController.ViewData.ModelState.AddModelError("Key", "ErrorMessage"); // Values of these two strings don't matter.  
    // What I'm doing is setting up the situation: my controller is receiving an invalid model.

    // act
    var result = (ViewResult) homeController.Index(p);

    // assert
    result.ForView("Index")
    Assert.That(result.ViewData.Model, Is.EqualTo(p));
}
192
ARM

J'avais eu le même problème, et après avoir lu la réponse et le commentaire de Paul, j'ai cherché un moyen de valider manuellement le modèle de vue.

J'ai trouvé ce tutoriel qui explique comment valider manuellement un ViewModel qui utilise DataAnnotations. L'extrait de code clé est vers la fin du message.

J'ai légèrement modifié le code - dans le tutoriel, le 4ème paramètre du TryValidateObject est omis (validateAllProperties). Afin d'obtenir toutes les annotations à valider, cela doit être défini sur true.

De plus, j'ai refactorisé le code dans une méthode générique, pour simplifier les tests de validation de ViewModel:

    public static void ValidateViewModel<TViewModel, TController>(this TController controller, TViewModel viewModelToValidate) 
        where TController : ApiController
    {
        var validationContext = new ValidationContext(viewModelToValidate, null, null);
        var validationResults = new List<ValidationResult>();
        Validator.TryValidateObject(viewModelToValidate, validationContext, validationResults, true);
        foreach (var validationResult in validationResults)
        {
            controller.ModelState.AddModelError(validationResult.MemberNames.FirstOrDefault() ?? string.Empty, validationResult.ErrorMessage);
        }
    }

Jusqu'à présent, cela a très bien fonctionné pour nous.

87
Giles Smith

Lorsque vous appelez la méthode homeController.Index dans votre test, vous n'utilisez aucun framework MVC qui déclenche la validation, donc ModelState.IsValid sera toujours vrai. Dans notre code, nous appelons une méthode d'aide Validate directement dans le contrôleur plutôt que d'utiliser la validation ambiante. Je n'ai pas beaucoup d'expérience avec les DataAnnotations (nous utilisons NHibernate.Validators) peut-être que quelqu'un d'autre peut offrir des conseils sur la façon d'appeler Validate depuis votre contrôleur.

7
Paul Alexander

Je faisais des recherches sur cela aujourd'hui et j'ai trouvé cet article de blog par Roberto Hernández (MVP) qui semble fournir la meilleure solution pour déclencher les validateurs pour une action de contrôleur pendant les tests unitaires. Cela mettra les erreurs correctes dans le ModelState lors de la validation d'une entité.

3
Darren

J'utilise ModelBinders dans mes cas de test pour pouvoir mettre à jour la valeur model.IsValid.

var form = new FormCollection();
form.Add("Name", "0123456789012345678901234567890123456789");

var model = MvcModelBinder.BindModel<AddItemModel>(controller, form);

ViewResult result = (ViewResult)controller.Add(model);

Avec ma méthode MvcModelBinder.BindModel comme suit (essentiellement le même code utilisé en interne dans le framework MVC):

        public static TModel BindModel<TModel>(Controller controller, IValueProvider valueProvider) where TModel : class
        {
            IModelBinder binder = ModelBinders.Binders.GetBinder(typeof(TModel));
            ModelBindingContext bindingContext = new ModelBindingContext()
            {
                FallbackToEmptyPrefix = true,
                ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(TModel)),
                ModelName = "NotUsedButNotNull",
                ModelState = controller.ModelState,
                PropertyFilter = (name => { return true; }),
                ValueProvider = valueProvider
            };

            return (TModel)binder.BindModel(controller.ControllerContext, bindingContext);
        }
2
ggarber

Si vous vous souciez de la validation mais que vous ne vous souciez pas de la façon dont elle est implémentée, si vous ne vous souciez que de la validation de votre méthode d'action au plus haut niveau d'abstraction, qu'elle soit implémentée comme utilisant DataAnnotations, ModelBinders ou même ActionFilterAttributes, alors vous pouvez utiliser le package de nuget Xania.AspNet.Simulator comme suit:

install-package Xania.AspNet.Simulator

-

var action = new BlogController()
    .Action(c => c.Index(new BlogPost()), "POST");
var modelState = action.ValidateRequest();

modelState.IsValid.Should().BeFalse();
1
Ibrahim ben Salah

Cela ne répond pas exactement à votre question, car il abandonne DataAnnotations, mais je vais l'ajouter car cela pourrait aider d'autres personnes à écrire des tests pour leurs contrôleurs:

Vous avez la possibilité de ne pas utiliser la validation fournie par System.ComponentModel.DataAnnotations mais toujours en utilisant l'objet ViewData.ModelState, en utilisant sa méthode AddModelError et un autre mécanisme de validation. Par exemple:

public ActionResult Create(CompetitionEntry competitionEntry)
{        
    if (competitionEntry.Email == null)
        ViewData.ModelState.AddModelError("CompetitionEntry.Email", "Please enter your e-mail");

    if (ModelState.IsValid)
    {
       // insert code to save data here...
       // ...

       return Redirect("/");
    }
    else
    {
        // return with errors
        var viewModel = new CompetitionEntryViewModel();
        // insert code to populate viewmodel here ...
        // ...


        return View(viewModel);
    }
}

Cela vous permet toujours de profiter de la fonction Html.ValidationMessageFor() générée par MVC, sans utiliser la fonction DataAnnotations. Vous devez vous assurer que la clé que vous utilisez avec AddModelError correspond à ce que la vue attend pour les messages de validation.

Le contrôleur devient alors testable parce que la validation se produit de manière explicite, plutôt que d'être effectuée automatiquement par le framework MVC.

1
codeulike

Je suis d'accord que ARM a la meilleure réponse: tester le comportement de votre contrôleur, pas la validation intégrée.

Cependant, vous pouvez également tester unitairement que votre modèle/ViewModel possède les bons attributs de validation définis. Disons que votre ViewModel ressemble à ceci:

public class PersonViewModel
{
    [Required]
    public string FirstName { get; set; }
}

Ce test unitaire testera l'existence de [Required] attribut:

[TestMethod]
public void FirstName_should_be_required()
{
    var propertyInfo = typeof(PersonViewModel).GetProperty("FirstName");

    var attribute = propertyInfo.GetCustomAttributes(typeof(RequiredAttribute), false)
                                .FirstOrDefault();

    Assert.IsNotNull(attribute);
}
1
Alex York

Contrairement à ARM, je n'ai pas de problème avec le creusage de tombes. Voici donc ma suggestion. Il s'appuie sur la réponse de Giles Smith et fonctionne pour ASP.NET MVC4 (je sais que la question concerne MVC 2, mais Google ne fait pas de discrimination lors de la recherche de réponses et je ne peux pas tester sur MVC2.) Au lieu de mettre le code de validation dans une méthode statique générique, je l'ai mise dans un contrôleur de test. Le contrôleur dispose de tout le nécessaire pour la validation. Ainsi, le contrôleur de test ressemble à ceci:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Wbe.Mvc;

protected class TestController : Controller
    {
        public void TestValidateModel(object Model)
        {
            ValidationContext validationContext = new ValidationContext(Model, null, null);
            List<ValidationResult> validationResults = new List<ValidationResult>();
            Validator.TryValidateObject(Model, validationContext, validationResults, true);
            foreach (ValidationResult validationResult in validationResults)
            {
                this.ModelState.AddModelError(String.Join(", ", validationResult.MemberNames), validationResult.ErrorMessage);
            }
        }
    }

Bien sûr, la classe n'a pas besoin d'être une classe interne protégée, c'est ainsi que je l'utilise maintenant, mais je vais probablement réutiliser cette classe. S'il y a quelque part un modèle MyModel qui est décoré d'attributs d'annotation de données Nice, alors le test ressemble à ceci:

    [TestMethod()]
    public void ValidationTest()
    {
        MyModel item = new MyModel();
        item.Description = "This is a unit test";
        item.LocationId = 1;

        TestController testController = new TestController();
        testController.TestValidateModel(item);

        Assert.IsTrue(testController.ModelState.IsValid, "A valid model is recognized.");
    }

L'avantage de cette configuration est que je peux réutiliser le contrôleur de test pour les tests de tous mes modèles et peut être en mesure de l'étendre pour se moquer un peu plus du contrôleur ou utiliser les méthodes protégées dont dispose un contrôleur.

J'espère que cela aide.

1
Albert

La réponse de @ giles-smith est mon approche préférée mais l'implémentation peut être simplifiée:

    public static void ValidateViewModel(this Controller controller, object viewModelToValidate)
    {
        var validationContext = new ValidationContext(viewModelToValidate, null, null);
        var validationResults = new List<ValidationResult>();
        Validator.TryValidateObject(viewModelToValidate, validationContext, validationResults, true);
        foreach (var validationResult in validationResults)
        {
            controller.ModelState.AddModelError(validationResult.MemberNames.FirstOrDefault() ?? string.Empty, validationResult.ErrorMessage);
        }
    }
0
Sam Shiles

D'après la réponse et les commentaires de @ giles-smith, pour l'API Web:

    public static void ValidateViewModel<TViewModel, TController>(this TController controller, TViewModel viewModelToValidate) 
        where TController : ApiController
    {
        var validationContext = new ValidationContext(viewModelToValidate, null, null);
        var validationResults = new List<ValidationResult>();
        Validator.TryValidateObject(viewModelToValidate, validationContext, validationResults, true);
        foreach (var validationResult in validationResults)
        {
            controller.ModelState.AddModelError(validationResult.MemberNames.FirstOrDefault() ?? string.Empty, validationResult.ErrorMessage);
        }
    }

Voir sur la réponse modifier ci-dessus ...

0
malix