Frage Unit-Tests zur MVC-Validierung


Wie kann ich testen, ob meine Controller-Aktion beim Validieren einer Entität die richtigen Fehler in ModelState einfügt, wenn ich die DataAnnotation-Validierung in MVC 2 Preview 1 verwende?

Etwas Code zur Veranschaulichung. Zuerst 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 versagender Komponententest, von dem ich denke, dass er passieren 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 teste die Validierung und sollte ich sie auf diese Weise testen?


75
2017-08-13 02:20


Ursprung


Antworten:


Anstatt ein zu geben BlogPost Sie können den Aktionsparameter auch als angeben FormCollection. Dann können Sie das erstellen BlogPost selbst und ruf an UpdateModel(model, formCollection.ToValueProvider());.

Dies löst die Validierung für jedes Feld in der 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 nur sicher, dass Ihr Test für jedes Feld in dem Ansichtsformular, das leer bleiben soll, einen Nullwert hinzufügt.

Ich habe festgestellt, dass meine Komponententests so aussehen, als ob der Code zur Laufzeit aufgerufen würde und sie dadurch noch wertvoller würden, und zwar auf Kosten von ein paar zusätzlichen Codezeilen. Sie können auch testen, was passiert, wenn jemand "abc" in ein Steuerelement eingibt, das an eine int-Eigenschaft gebunden ist.


-3
2017-08-13 07:32



Hasse es, einen alten Post zu nekroven, aber ich dachte, ich würde meine eigenen Gedanken hinzufügen (da ich gerade dieses Problem hatte und über diesen Post gestolpert bin, während ich die Antwort gesucht habe).

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

Was Sie hier wirklich testen möchten, ist, dass Ihr Controller das tut, was Sie erwarten, wenn die Validierung fehlschlägt. Das ist dein Code und deine Erwartungen. Testen Sie es einfach, wenn Sie feststellen, dass es 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));
}

189
2017-09-28 19:02



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 fand dieses Tutorial Hier erfahren Sie, wie Sie ein ViewModel, das DataAnnotations verwendet, manuell validieren. Das Key Code Snippet ist gegen Ende des Posts.

Ich habe den Code leicht geändert - im Tutorial wurde der vierte Parameter des TryValidateObject weggelassen (validateAllProperties). Um alle Annotationen auf Validate zu setzen, sollte dies auf "true" gesetzt sein.

Außerdem habe ich den Code in eine generische Methode umgewandelt, 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);
        }
    }

Bis jetzt hat das wirklich gut für uns funktioniert.


83
2017-07-28 13:08



Wenn Sie die homeController.Index-Methode in Ihrem Test aufrufen, verwenden Sie keines des MVC-Frameworks, das die Überprüfung auslöst, damit ModelState.IsValid immer wahr ist. In unserem Code rufen wir eine Helfer-Validate-Methode direkt im Controller auf, anstatt die Umgebungsvalidierung zu verwenden. Ich hatte nicht viel Erfahrung mit den DataAnnotations (wir verwenden NHibernate.Validators) vielleicht kann jemand anderes Anleitung anbieten, wie man Validate von innerhalb Ihres Controllers aufruft.


6
2017-08-13 03:58



Ich habe das heute untersucht und gefunden dieser Blogbeitrag von Roberto Hernández (MVP), die die beste Lösung zu bieten scheint, um die Prüfer für eine Controller-Aktion während des Komponententests auszulösen. Dadurch werden beim Validieren einer Entität die korrekten Fehler in ModelState angezeigt.


3
2017-10-05 03:45



Ich verwende ModelBinders in meinen Testfällen, um den Wert von model.IsValid zu aktualisieren.

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 der gleiche Code verwendet intern im MVC-Framework):

        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
2018-02-18 19:50



Dies beantwortet Ihre Frage nicht genau, weil DataAnnotations aufgegeben wird, aber ich füge es hinzu, weil es anderen Leuten 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 das ViewData.ModelState-Objekt weiterhin zu verwenden, indem Sie dessen verwenden AddModelError Methode und einige andere Validierungsmechanismen. 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);
    }
}

Dadurch können Sie immer noch die Vorteile der Html.ValidationMessageFor() Sachen, die MVC generiert, ohne die DataAnnotations. Sie müssen sicherstellen, dass der Schlüssel mit verwendet wird AddModelError stimmt mit dem überein, was die Ansicht für Validierungsnachrichten erwartet.

Der Controller wird dann testbar, da die Validierung explizit stattfindet und nicht automatisch vom MVC-Framework ausgeführt wird.


1
2017-09-23 19:52



Ich stimme zu, dass ARM die beste Antwort hat: Testen Sie das Verhalten Ihres Controllers, nicht die eingebaute Validierung.

Sie können jedoch auch testen, ob das Modell / ViewModel über die richtigen Validierungsattribute verfügt. Nehmen wir an, Ihr ViewModel sieht folgendermaßen aus:

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

Dieser Komponententest wird auf die Existenz des [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
2018-05-26 16:41



Im Gegensatz zu ARM habe ich kein Problem mit Graben. Also hier ist mein Vorschlag. Es baut auf der Antwort von Giles Smith auf und arbeitet für ASP.NET MVC4 (ich weiß, dass die Frage über MVC 2 ist, aber Google diskriminiert nicht, wenn er nach Antworten sucht und ich kann nicht auf MVC2 testen.) Anstatt den Validierungscode in eine generische statische Methode zu schreiben, lege ich ihn in einen Testcontroller. 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, das ist die Art, wie ich sie jetzt benutze, aber wahrscheinlich werde ich diese Klasse wiederverwenden. Wenn irgendwo ein Modell MyModel vorhanden ist, das mit netten Datenanmerkungsattributen versehen ist, dann 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 Test-Controller für Tests aller meiner Modelle wiederverwenden kann und ihn möglicherweise erweitern kann, um ein wenig mehr über den Controller nachzuhacken oder die geschützten Methoden eines Controllers zu verwenden.

Ich hoffe es hilft.


1
2018-03-21 20:38



Wenn Ihnen die Validierung wichtig ist, Sie aber nicht daran interessiert sind, wie sie implementiert wird, wenn Sie nur die Validierung Ihrer Aktionsmethode auf der höchsten Abstraktionsebene durchführen möchten, egal ob sie mit DataAnnotations, ModelBinders oder gar 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();

1
2017-08-01 22:04



Basierend auf @ Giles-Smiths Antwort und Kommentaren für 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 auf Antwort Bearbeiten oben ...


0
2018-03-30 20:10