Unit-Tests zur MVC-Validierung

77

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?

Matthew Groves
quelle
5
Ist nicht var p = new BlogPost {Title = "test"}; mehr arrangieren als handeln?
RichardOD
1
Assert.IsFalse (homeController.ModelState.IsValid);
Seth Flowers

Antworten:

-4

Anstatt a zu übergeben BlogPost, können Sie den Aktionsparameter auch als deklarieren FormCollection. Dann können Sie sich selbst erstellen BlogPostund 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.

Maurice
quelle
2
Ich mag diesen Ansatz, aber es scheint ein Schritt zurück oder mindestens ein zusätzlicher Schritt zu sein, den ich in jede Aktion einfügen muss, die POST behandelt.
Matthew Groves
2
Genau. Aber es lohnt sich, meine Unit-Tests und die echte App auf die gleiche Weise zu haben.
Maurice
5
ARMs Ansatz ist
meiner Meinung nach
4
Diese Art besiegt den Zweck von MVC.
Andy
2
Ich stimme zu, dass die Antwort von ARM besser ist. Das Übergeben einer FormCollection an eine Controller-Aktion ist im Vergleich zum Übergeben eines stark typisierten Model / ViewModel-Objekts unerwünscht.
Alex York
194

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).

  1. Testen Sie die Validierung nicht in Ihren Controller-Tests. Entweder Sie vertrauen der Validierung von MVC oder Sie schreiben Ihren eigenen (dh testen Sie nicht den Code anderer, testen Sie Ihren Code)
  2. Wenn Sie testen möchten, ob die Validierung das tut, was Sie erwarten, testen Sie sie in Ihren Modelltests (ich mache dies für einige meiner komplexeren Regex-Validierungen).

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));
}
ARM
quelle
2
Ich stimme zu, dies sollte die richtige Antwort sein. Wie ARM sagt: Die eingebaute Validierung sollte nicht getestet werden. Stattdessen sollte das Verhalten Ihres Controllers getestet werden. Das macht am meisten Sinn.
Alex York
Der Controller sollte getrennt von der Modellbindung und -validierung getestet werden. Folgt sowohl KISS als auch Trennung der Bedenken. Ich mache eine kleine Reihe von Artikeln über Unit-Tests MVC-Komponenten hier timoch.com/blog/2013/06/…
TiMoch
3
Was sollten Sie tun, um benutzerdefinierte Validierungsattribute zu testen? Wenn diese verwendet werden, kann man der Validierung von MVC nicht "vertrauen". Wie würden Sie (vermutlich in den Modelltests) testen, ob die benutzerdefinierte Validierung funktioniert?
John Saunders
2
Ich stimme nicht zu. Wir müssen noch überprüfen, ob ein bestimmtes Modell die Modellfehler erzeugt, die als Voraussetzung für diesen Test verwendet werden. Der Beispielcode ist jedoch eine perfekte Antwort auf Ihre eigene definierte Frage in 1. Es ist jedoch nicht die Antwort auf die ursprüngliche Frage
Ibrahim ben Salah
Dies testet nicht die Modellvalidierung. In diesem Fall könnte jemand (absichtlich oder versehentlich) eine Datenanmerkung im Modell entfernen (möglicherweise ein Zusammenführungsfehler?), Und dieser Test schlägt nicht fehl.
Rosdi Kasim
89

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.

Giles Smith
quelle
Entschuldigung, hatte das nicht einmal überprüft. Alle unsere MVC-Projekte sind in 4.0
Giles Smith
Danke dafür! Ein kleiner Nachtrag; Wenn Sie eine Validierung haben, die nicht an ein bestimmtes Feld gekoppelt ist (dh Sie haben IValidatableObject implementiert), sind die Mitgliedsnamen leer, und der Modellfehlerschlüssel sollte eine leere Zeichenfolge sein. In der foreach können Sie tun: var key = validationResult.MemberNames.Any() ? validationResult.MemberNames.First() : string.Empty; controller.ModelState.AddModelError(key, validationResult.ErrorMessage);
Thomas Lundström
6
Warum muss das Generika verwenden? Dies könnte viel einfacher genutzt werden, wenn es definiert wäre als: void ValidateViewModel (Objekt viewModelToValidate, Controller-Controller) oder noch besser als Erweiterungsmethode: public static void ValidateViewModel (dieser Controller-Controller, Objekt viewModelToValidate)
Chad Grant
Das ist großartig, aber ich stimme Chad zu, nur die generische Syntax loszuwerden.
Roger
Wenn jemand das gleiche Problem wie ich mit dem "Validator" hatte, verwenden Sie "System.ComponentModel.DataAnnotations.Validator.TryValidateObject", um sicherzustellen, dass Sie den richtigen Validator verwenden.
Alin Ciocan
7

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.

Paul Alexander
quelle
Ich mag den Begriff "Umgebungsvalidierung". Aber muss es eine Möglichkeit geben, dies in einem Unit-Test auszulösen?
Matthew Groves
3
Das Problem ist jedoch, dass Sie im Grunde das MVC-Framework testen - nicht Ihren Controller. Sie versuchen zu bestätigen, dass MVC Ihr Modell wie erwartet validiert. Die einzige Möglichkeit, dies mit Sicherheit zu tun, besteht darin, die gesamte MVC-Pipeline zu verspotten und eine Webanforderung zu simulieren. Das ist wahrscheinlich mehr als Sie wirklich wissen müssen. Wenn Sie nur testen, ob die Datenüberprüfung auf Ihren Modellen korrekt eingerichtet ist, können Sie dies ohne den Controller tun und die Datenüberprüfung einfach manuell ausführen.
Paul Alexander
3

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.

Darren
quelle
2

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);
        }
Ggarber
quelle
Dies funktioniert nicht, wenn Sie mehr als ein Validierungsattribut für eine Eigenschaft haben. Fügen Sie diese Zeile controller.ModelState.Clear();vor dem Code hinzu, der erstellt wird, ModelBindingContextund es würde funktionieren
Suhas
1

Dies 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 AddModelErrorMethode 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 verwenden DataAnnotations. 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.

codeulike
quelle
Wenn Sie die Validierung auf diese Weise durchführen, werden einige der besten Teile der Validierung in MVC weggeworfen. Ich möchte eine Validierung für mein Modell hinzufügen, nicht für den Controller. Wenn ich diese Lösung verwende, werden viele mögliche Code-Duplikate mit den dazugehörigen Albträumen auftreten.
Willem Meints
@ W.Meints: Richtig, aber die Codezeilen im obigen Beispiel, die die Validierung durchführen, können auf Wunsch auch in eine Methode im Modell verschoben werden. Der Punkt ist, dass die Validierung über Code und nicht über Attribute testbarer wird. Paul erklärt es besser über stackoverflow.com/a/1269960/22194
codeulike
1

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);
}
Alex York
quelle
Wie werden wir dann die integrierte Validierung testen? Besonders wenn wir es mit zusätzlichen Attributen, Fehlermeldungen usw. angepasst haben
Teoman shipahi
1

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.

Albert
quelle
1

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();
Ibrahim ben Salah
quelle
0

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 ...

Malix
quelle
0

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);
        }
    }
Sam Shiles
quelle