Wie verspotte ich ModelState.IsValid mit dem Moq-Framework?

88

Ich überprüfe ModelState.IsValidmeine Controller-Aktionsmethode, mit der ein Mitarbeiter wie folgt erstellt wird:

[HttpPost]
public virtual ActionResult Create(EmployeeForm employeeForm)
{
    if (this.ModelState.IsValid)
    {
        IEmployee employee = this._uiFactoryInstance.Map(employeeForm);
        employee.Save();
    }

    // Etc.
}

Ich möchte es in meiner Unit-Test-Methode mit Moq Framework verspotten. Ich habe versucht, es so zu verspotten:

var modelState = new Mock<ModelStateDictionary>();
modelState.Setup(m => m.IsValid).Returns(true);

Dies löst jedoch eine Ausnahme in meinem Unit-Testfall aus. Kann mir hier jemand helfen?

Mazen
quelle

Antworten:

140

Sie müssen es nicht verspotten. Wenn Sie bereits einen Controller haben, können Sie beim Initialisieren Ihres Tests einen Modellstatusfehler hinzufügen:

// arrange
_controllerUnderTest.ModelState.AddModelError("key", "error message");

// act
// Now call the controller action and it will 
// enter the (!ModelState.IsValid) condition
var actual = _controllerUnderTest.Index();
Darin Dimitrov
quelle
Wie setzen wir ModelState.IsValid so, dass es den wahren Fall trifft? ModelState hat keinen Setter und daher können wir Folgendes nicht tun: _controllerUnderTest.ModelState.IsValid = true. Ohne das wird es den Angestellten nicht treffen
Karan
4
@ Newton, es ist standardmäßig wahr. Sie müssen nichts angeben, um den wahren Fall zu treffen. Wenn Sie den falschen Fall treffen möchten, fügen Sie einfach einen Modellstatusfehler hinzu, wie in meiner Antwort gezeigt.
Darin Dimitrov
IMHO Bessere Lösung ist die Verwendung von MVC-Förderer. Auf diese Weise erhalten Sie ein realistischeres Verhalten Ihres Controllers. Sie sollten die Modellvalidierung für das Schicksal bereitstellen - Attributvalidierungen. Der folgende Beitrag beschreibt dies ( stackoverflow.com/a/5580363/572612 )
Vladimir Shmidt
13

Das einzige Problem, das ich mit der obigen Lösung habe, ist, dass das Modell nicht getestet wird, wenn ich Attribute festlege. Ich richte meinen Controller auf diese Weise ein.

private HomeController GenerateController(object model)
    {
        HomeController controller = new HomeController()
        {
            RoleService = new MockRoleService(),
            MembershipService = new MockMembershipService()
        };
        MvcMockHelpers.SetFakeAuthenticatedControllerContext(controller);

        // bind errors modelstate to the controller
        var modelBinder = new ModelBindingContext()
        {
            ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()),
            ValueProvider = new NameValueCollectionValueProvider(new NameValueCollection(), CultureInfo.InvariantCulture)
        };
        var binder = new DefaultModelBinder().BindModel(new ControllerContext(), modelBinder);
        controller.ModelState.Clear();
        controller.ModelState.Merge(modelBinder.ModelState);
        return controller;
    }

Das modelBinder-Objekt ist das Objekt, das die Gültigkeit des Modells testet. Auf diese Weise kann ich einfach die Werte des Objekts einstellen und es testen.

uadrive
quelle
1
Sehr schön, genau das habe ich gesucht. Ich weiß nicht, wie viele Leute eine alte Frage wie diese posten, aber sie hat Sie für mich geschätzt. Vielen Dank.
W. Jackson
Scheint eine großartige Lösung zu sein, immer noch im Jahr 2016 :)
Matt
2
Ist es nicht besser, das Modell isoliert mit so etwas zu testen? stackoverflow.com/a/4331964/3198973
RubberDuck
2
Obwohl dies eine clevere Lösung ist, stimme ich @RubberDuck zu. Damit dies ein tatsächlicher, isolierter Komponententest ist, sollte die Validierung des Modells ein eigener Test sein, während der Test des Controllers eigene Tests haben sollte. Wenn sich das Modell ändert, um die ModelBinder-Validierung zu verletzen, schlägt Ihr Controller-Test fehl. Dies ist falsch positiv, da die Controller-Logik nicht fehlerhaft ist. Um ein ungültiges ModelStateDictionary zu testen, fügen Sie einfach einen gefälschten ModelState-Fehler hinzu, damit die Prüfung ModelState.IsValid fehlschlägt.
xDaevax
2

Die Antwort von uadrive hat mich ein Stück weit gebracht, aber es gab immer noch einige Lücken. Ohne Daten in der Eingabe für new NameValueCollectionValueProvider()bindet der Modellordner den Controller an ein leeres Modell und nicht an das modelObjekt.

Das ist in Ordnung - serialisieren Sie Ihr Modell einfach als NameValueCollectionund übergeben Sie es dann an den NameValueCollectionValueProviderKonstruktor. Nicht ganz. Leider hat es in meinem Fall nicht funktioniert, da mein Modell eine Sammlung enthält und die NameValueCollectionValueProvidernicht gut mit Sammlungen zusammenspielt.

Das JsonValueProviderFactorykommt hier allerdings zur Rettung. Es kann von verwendet werden, DefaultModelBindersolange Sie einen Inhaltstyp von "application/json" angeben und Ihr serialisiertes JSON-Objekt an den Eingabestream Ihrer Anforderung übergeben (Bitte beachten Sie, dass es sich bei diesem Eingabestream um einen Speicherstrom handelt, da er als Speicher nicht verfügbar ist Stream hält keine externen Ressourcen fest):

protected void BindModel<TModel>(Controller controller, TModel viewModel)
{
    var controllerContext = SetUpControllerContext(controller, viewModel);
    var bindingContext = new ModelBindingContext
    {
        ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => viewModel, typeof(TModel)),
        ValueProvider = new JsonValueProviderFactory().GetValueProvider(controllerContext)
    };

    new DefaultModelBinder().BindModel(controller.ControllerContext, bindingContext);
    controller.ModelState.Clear();
    controller.ModelState.Merge(bindingContext.ModelState);
}

private static ControllerContext SetUpControllerContext<TModel>(Controller controller, TModel viewModel)
{
    var controllerContext = A.Fake<ControllerContext>();
    controller.ControllerContext = controllerContext;
    var json = new JavaScriptSerializer().Serialize(viewModel);
    A.CallTo(() => controllerContext.Controller).Returns(controller);
    A.CallTo(() => controllerContext.HttpContext.Request.InputStream).Returns(new MemoryStream(Encoding.UTF8.GetBytes(json)));
    A.CallTo(() => controllerContext.HttpContext.Request.ContentType).Returns("application/json");
    return controllerContext;
}
Rob Lyndon
quelle