Tests unitaires sur la validation MVC

Comment puis-je tester que l’action de mon 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?

Un code à 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 d’unité défaillant qui, je pense, devrait être passé mais pas (en utilisant MbUnit & Moq):

 [Test] public void When_processing_invalid_post_HomeControllerModelState_should_have_at_least_one_error() { // arrange var mockRepository = new Mock(); 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, devrais- je tester la validation et devrais-je la tester de cette façon?

Au lieu de transmettre un BlogPost vous pouvez également déclarer le paramètre actions en tant que FormCollection . Vous pouvez ensuite créer vous-même BlogPost et appeler UpdateModel(model, formCollection.ToValueProvider()); .

Cela déclenchera la validation pour n’importe quel champ dans FormCollection .

  [HttpPost] public ActionResult Index(FormCollection form) { var b = new BlogPost(); TryUpdateModel(model, form.ToValueProvider()); if (ModelState.IsValid) { _blogService.Insert(b); return (View("Success", b)); } return View(b); } 

Assurez-vous simplement que votre test ajoute une valeur nulle pour chaque champ du formulaire de vues que vous souhaitez laisser vide.

J’ai trouvé que le faire de cette façon, au prix de quelques lignes de code supplémentaires, fait que mes tests unitaires ressemblent davantage à la manière dont le code est appelé à l’exécution, ce qui les rend plus précieux. Vous pouvez également tester ce qui se passe quand quelqu’un entre “abc” dans un contrôle lié à une propriété int.

Je déteste necro un vieux post, mais je pensais append mes propres pensées (puisque je viens d’avoir ce problème et que je suis tombé sur 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 vous-même (c’est-à-dire que vous ne testez pas le code d’un autre, testez votre code)
  2. Si vous voulez tester la validation, faites ce que vous attendez, testez-la dans vos tests de modèle (je le fais pour quelques unes de mes validations de regex plus complexes).

Ce que vous voulez vraiment tester, c’est que votre contrôleur fait ce que vous attendez de lui lorsque la validation échoue. C’est votre code et vos attentes. Tester c’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(); var homeController = new HomeController(mockRepository.Object); var p = new BlogPost(); homeController.ViewData.ModelState.AddModelError("Key", "ErrorMessage"); // Values of these two ssortingngs 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)); } 

J’avais le même problème et après avoir lu la réponse et les commentaires 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. Ils L’extrait de code est vers la fin de la publication.

J’ai légèrement modifié le code – dans le tutoriel, le 4ème paramètre de TryValidateObject est omis (validateAllProperties). Pour que toutes les annotations soient validées, cela doit être défini sur true.

Additionaly j’ai refactorisé le code dans une méthode générique, pour que le test de validation ViewModel soit simple:

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

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

Lorsque vous appelez la méthode homeController.Index dans votre test, vous n’utilisez aucune structure MVC qui déclenche la validation, de sorte que ModelState.IsValid sera toujours vrai. Dans notre code, nous appelons une méthode d’assistance Validate directement dans le contrôleur plutôt que d’utiliser la validation ambiante. Je n’ai pas eu beaucoup d’expérience avec les DataAnnotations (Nous utilisons NHibernate.Validators), peut-être que quelqu’un d’autre peut vous conseiller sur comment appeler Validate depuis votre contrôleur.

Je faisais des recherches à ce sujet aujourd’hui et j’ai trouvé ce billet de 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é.

J’utilise ModelBinders dans mes scénarios de test pour pouvoir mettre à jour la valeur model.IsValid.

 var form = new FormCollection(); form.Add("Name", "0123456789012345678901234567890123456789"); var model = MvcModelBinder.BindModel(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(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); } 

Cela ne répond pas exactement à votre question, car elle abandonne DataAnnotations, mais je vais l’append 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 d’utiliser toujours l’object 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 tirer parti des fonctionnalités de Html.ValidationMessageFor() générées par MVC, sans utiliser les DataAnnotations . Vous devez vous assurer que la clé que vous utilisez avec AddModelError correspond à ce que la vue attend des messages de validation.

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

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 que votre modèle / ViewModel possède les atsortingbuts de validation corrects définis. Disons que votre ViewModel ressemble à ceci:

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

Ce test unitaire testera l’existence de l’atsortingbut [Required] :

 [TestMethod] public void FirstName_should_be_required() { var propertyInfo = typeof(PersonViewModel).GetProperty("FirstName"); var atsortingbute = propertyInfo.GetCustomAtsortingbutes(typeof(RequiredAtsortingbute), false) .FirstOrDefault(); Assert.IsNotNull(atsortingbute); } 

Contrairement à l’ARM, je n’ai pas de problème avec le creusage des tombes. Alors voici 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 distinction lors de la recherche de réponses et je ne peux pas tester sur MVC2). une méthode statique générique, je la mets dans un contrôleur de test. Le contrôleur a tout ce qu’il faut pour la validation. Donc, 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 validationResults = new List(); Validator.TryValidateObject(Model, validationContext, validationResults, true); foreach (ValidationResult validationResult in validationResults) { this.ModelState.AddModelError(Ssortingng.Join(", ", validationResult.MemberNames), validationResult.ErrorMessage); } } } 

Bien sûr, la classe n’a pas besoin d’être une classe interne protégée, c’est comme ça que je l’utilise maintenant, mais je vais probablement réutiliser cette classe. Si quelque part il y a un modèle MyModel décoré avec de jolis atsortingbuts d’annotation de données, 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 pouvoir l’étendre pour me moquer un peu plus du contrôleur ou utiliser les méthodes protégées d’un contrôleur.

J’espère que cela aide.

Si vous vous souciez de la validation mais que vous ne vous souciez pas de son implémentation, 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 avec DataAnnotations, ModelBinders ou même ActionFilterAtsortingbutes, alors Vous pouvez utiliser le package 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(); 

Basé sur la réponse et les commentaires de @giles-smith, pour Web API:

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

Voir la réponse modifier ci-dessus …

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(); Validator.TryValidateObject(viewModelToValidate, validationContext, validationResults, true); foreach (var validationResult in validationResults) { controller.ModelState.AddModelError(validationResult.MemberNames.FirstOrDefault() ?? ssortingng.Empty, validationResult.ErrorMessage); } }