Wie kann ich testen, ob meine Controller-Aktion beim Validieren einer Entität die richtigen Fehler in den ModelState einfügt, wenn ich die DataAnnotation-Validierung in MVC 2 Preview 1 verwende?
Ein Code zur Veranschaulichung. Erstens die Aktion:
[HttpPost]
public ActionResult Index(BlogPost b)
{
if(ModelState.IsValid)
{
_blogService.Insert(b);
return(View("Success", b));
}
return View(b);
}
Und hier ist ein fehlgeschlagener Unit-Test, der meiner Meinung nach bestanden werden sollte, aber nicht (mit 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);
}
Ich denke, zusätzlich zu dieser Frage sollte ich die Validierung testen und sollte ich sie auf diese Weise testen?
asp.net-mvc
unit-testing
validation
asp.net-mvc-2
tdd
Matthew Groves
quelle
quelle
Antworten:
Anstatt a zu übergeben
BlogPost
, können Sie den Aktionsparameter auch als deklarierenFormCollection
. Dann können Sie sich selbst erstellenBlogPost
und anrufenUpdateModel(model, formCollection.ToValueProvider());
.Dies löst die Validierung für jedes Feld in der aus
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); }
Stellen Sie einfach sicher, dass Ihr Test für jedes Feld im Ansichtsformular, das Sie leer lassen möchten, einen Nullwert hinzufügt.
Ich habe festgestellt, dass meine Unit-Tests auf diese Weise auf Kosten einiger zusätzlicher Codezeilen der Art und Weise ähneln, wie der Code zur Laufzeit aufgerufen wird, wodurch sie wertvoller werden. Sie können auch testen, was passiert, wenn jemand "abc" in ein Steuerelement eingibt, das an eine int-Eigenschaft gebunden ist.
quelle
Ich hasse es, einen alten Beitrag zu nekrotisieren, aber ich dachte, ich würde meine eigenen Gedanken hinzufügen (da ich gerade dieses Problem hatte und auf diesen Beitrag stieß, während ich nach der Antwort suchte).
Was Sie hier wirklich testen möchten, ist, dass Ihr Controller das tut, was Sie von ihm erwarten, wenn die Validierung fehlschlägt. Das ist Ihr Code und Ihre Erwartungen. Das Testen ist einfach, sobald Sie feststellen, dass dies alles ist, was Sie testen möchten:
[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)); }
quelle
Ich hatte das gleiche Problem und nachdem ich Pauls Antwort und Kommentar gelesen hatte, suchte ich nach einer Möglichkeit, das Ansichtsmodell manuell zu validieren.
Ich habe dieses Tutorial gefunden, in dem erklärt wird, wie ein ViewModel, das DataAnnotations verwendet, manuell überprüft wird. Das Schlüsselcode-Snippet befindet sich gegen Ende des Beitrags.
Ich habe den Code leicht geändert - im Tutorial wird der 4. Parameter des TryValidateObject weggelassen (validateAllProperties). Damit alle Anmerkungen validiert werden, sollte dies auf true gesetzt werden.
Zusätzlich habe ich den Code in eine generische Methode umgestaltet, um das Testen der ViewModel-Validierung zu vereinfachen:
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); } }
Bisher hat das bei uns sehr gut funktioniert.
quelle
var key = validationResult.MemberNames.Any() ? validationResult.MemberNames.First() : string.Empty; controller.ModelState.AddModelError(key, validationResult.ErrorMessage);
Wenn Sie in Ihrem Test die Methode homeController.Index aufrufen, verwenden Sie kein MVC-Framework, das die Validierung auslöst, sodass ModelState.IsValid immer wahr ist. In unserem Code rufen wir eine Hilfs-Validierungsmethode direkt im Controller auf, anstatt die Umgebungsvalidierung zu verwenden. Ich habe nicht viel Erfahrung mit den DataAnnotations (wir verwenden NHibernate.Validators). Vielleicht kann jemand anderes Ihnen Anleitungen geben, wie Sie Validate von Ihrem Controller aus aufrufen können.
quelle
Ich habe dies heute recherchiert und diesen Blog-Beitrag von Roberto Hernández (MVP) gefunden, der die beste Lösung zu bieten scheint, um die Validatoren für eine Controller-Aktion während des Unit-Tests auszulösen. Dadurch werden beim Überprüfen einer Entität die richtigen Fehler in den ModelState eingefügt.
quelle
Ich verwende ModelBinders in meinen Testfällen, um den Wert model.IsValid aktualisieren zu können.
var form = new FormCollection(); form.Add("Name", "0123456789012345678901234567890123456789"); var model = MvcModelBinder.BindModel<AddItemModel>(controller, form); ViewResult result = (ViewResult)controller.Add(model);
Mit meiner MvcModelBinder.BindModel-Methode wie folgt (im Grunde derselbe Code, der intern im MVC-Framework verwendet wird):
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); }
quelle
controller.ModelState.Clear();
vor dem Code hinzu, der erstellt wird,ModelBindingContext
und es würde funktionierenDies beantwortet Ihre Frage nicht genau, da DataAnnotations aufgegeben wird, aber ich werde es hinzufügen, da es anderen Personen helfen könnte, Tests für ihre Controller zu schreiben:
Sie haben die Möglichkeit, die von System.ComponentModel.DataAnnotations bereitgestellte Validierung nicht zu verwenden, aber dennoch das ViewData.ModelState-Objekt mithilfe seiner
AddModelError
Methode und eines anderen Validierungsmechanismus zu verwenden. Z.B: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); } }
Auf diese Weise können Sie weiterhin die von
Html.ValidationMessageFor()
MVC generierten Inhalte nutzen, ohne die zu verwendenDataAnnotations
. Sie müssen sicherstellen, dass der Schlüssel, mit dem Sie verwendenAddModelError
den Erwartungen der Ansicht für Validierungsnachrichten übereinstimmt.Der Controller kann dann getestet werden, da die Validierung explizit erfolgt und nicht automatisch vom MVC-Framework durchgeführt wird.
quelle
Ich bin damit einverstanden, dass ARM die beste Antwort hat: Testen Sie das Verhalten Ihres Controllers, nicht die integrierte Validierung.
Sie können jedoch auch einen Komponententest durchführen, um sicherzustellen, dass für Ihr Model / ViewModel die richtigen Validierungsattribute definiert sind. Angenommen, Ihr ViewModel sieht folgendermaßen aus:
public class PersonViewModel { [Required] public string FirstName { get; set; } }
Dieser Komponententest prüft die Existenz des
[Required]
Attributs:[TestMethod] public void FirstName_should_be_required() { var propertyInfo = typeof(PersonViewModel).GetProperty("FirstName"); var attribute = propertyInfo.GetCustomAttributes(typeof(RequiredAttribute), false) .FirstOrDefault(); Assert.IsNotNull(attribute); }
quelle
Im Gegensatz zu ARM habe ich kein Problem mit dem Graben. Also hier ist mein Vorschlag. Es baut auf der Antwort von Giles Smith auf und funktioniert für ASP.NET MVC4 (Ich weiß, dass es sich bei der Frage um MVC 2 handelt, aber Google unterscheidet nicht bei der Suche nach Antworten und kann nicht auf MVC2 testen.) Anstatt den Validierungscode einzugeben Als generische statische Methode habe ich sie in einen Testcontroller gesteckt. Der Controller verfügt über alles, was zur Validierung benötigt wird. Der Testcontroller sieht also folgendermaßen aus:
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); } } }
Natürlich muss die Klasse keine geschützte innere Klasse sein, so benutze ich sie jetzt, aber ich werde diese Klasse wahrscheinlich wiederverwenden. Wenn es irgendwo ein Modell MyModel gibt, das mit schönen Datenanmerkungsattributen dekoriert ist, sieht der Test ungefähr so aus:
[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."); }
Der Vorteil dieses Setups besteht darin, dass ich den Testcontroller für Tests aller meiner Modelle wiederverwenden und ihn möglicherweise erweitern kann, um ein bisschen mehr über den Controller zu verspotten oder die geschützten Methoden eines Controllers zu verwenden.
Ich hoffe es hilft.
quelle
Wenn Sie sich für die Validierung interessieren, sich aber nicht für die Implementierung interessieren, wenn Sie sich nur für die Validierung Ihrer Aktionsmethode auf der höchsten Abstraktionsebene interessieren, unabhängig davon, ob sie mithilfe von DataAnnotations, ModelBinders oder sogar ActionFilterAttributes implementiert wird Sie können das Xania.AspNet.Simulator-Nuget-Paket wie folgt verwenden:
install-package Xania.AspNet.Simulator
- -
var action = new BlogController() .Action(c => c.Index(new BlogPost()), "POST"); var modelState = action.ValidateRequest(); modelState.IsValid.Should().BeFalse();
quelle
Basierend auf der Antwort und den Kommentaren von @ giles-smith für die Web-API:
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); } }
Siehe Antwort oben bearbeiten ...
quelle
Die Antwort von @ giles-smith ist mein bevorzugter Ansatz, aber die Implementierung kann vereinfacht werden:
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); } }
quelle