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?
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).
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));
}
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.
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.
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é.
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);
}
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();
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.
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);
}
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.
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);
}
}
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 ...