Come deridere ModelState.IsValid usando il framework Moq?


91

Sto controllando ModelState.IsValidil metodo di azione del controller che crea un dipendente come questo:

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

    // Etc.
}

Voglio deriderlo nel mio metodo di unit test utilizzando Moq Framework. Ho provato a deriderlo in questo modo:

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

Ma questo genera un'eccezione nel mio caso di unit test. Qualcuno può aiutarmi qui?

Risposte:


142

Non hai bisogno di deriderlo. Se hai già un controller, puoi aggiungere un errore di stato del modello durante l'inizializzazione del test:

// 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();

come impostiamo ModelState.IsValid per colpire il caso vero? ModelState non ha un setter e quindi non possiamo fare quanto segue: _controllerUnderTest.ModelState.IsValid = true. Senza questo, non colpirà il dipendente
Karan

4
@ Newton, è vero per impostazione predefinita. Non è necessario specificare nulla per colpire il caso vero. Se vuoi colpire il caso falso, aggiungi semplicemente un errore di stato del modello come mostrato nella mia risposta.
Darin Dimitrov

IMHO La soluzione migliore è utilizzare il trasportatore mvc. In questo modo ottieni un comportamento più realistico del tuo controller, dovresti fornire la convalida del modello al suo destino: le convalide degli attributi. Di seguito il post descrive questo ( stackoverflow.com/a/5580363/572612 )
Vladimir Shmidt

13

L'unico problema che ho con la soluzione sopra è che in realtà non verifica il modello se imposto gli attributi. Ho impostato il mio controller in questo modo.

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;
    }

L'oggetto modelBinder è l'oggetto che verifica la validità del modello. In questo modo posso semplicemente impostare i valori dell'oggetto e testarlo.


1
Molto carino, questo è esattamente quello che stavo cercando. Non so quante persone pubblicano una vecchia domanda come questa, ma per me aveva valore. Grazie.
W.Jackson

Sembra un'ottima soluzione, ancora nel 2016 :)
Matt

2
Non è meglio testare il modello da solo con qualcosa di simile? stackoverflow.com/a/4331964/3198973
RubberDuck

2
Sebbene questa sia una soluzione intelligente, sono d'accordo con @RubberDuck. Affinché questo sia un test unitario effettivo e isolato, la convalida del modello dovrebbe essere il suo test, mentre il test del controller dovrebbe avere i suoi test. Se il modello cambia per violare la convalida ModelBinder, il test del controller fallirà, il che è un falso positivo poiché la logica del controller non è interrotta. Per testare un ModelStateDictionary non valido, aggiungi semplicemente un errore ModelState falso affinché il controllo ModelState.IsValid fallisca.
xDaevax

2

La risposta di uadrive mi ha portato in parte, ma c'erano ancora delle lacune. Senza alcun dato nell'input a new NameValueCollectionValueProvider(), il raccoglitore del modello legherà il controller a un modello vuoto, non modelall'oggetto.

Va bene: serializza il tuo modello come a NameValueCollection, quindi passalo al NameValueCollectionValueProvidercostruttore. Beh, non proprio. Sfortunatamente, nel mio caso non ha funzionato perché il mio modello contiene una raccolta e NameValueCollectionValueProvidernon funziona bene con le raccolte.

Ma qui JsonValueProviderFactoryviene in soccorso. Puòessere usato DefaultModelBinderfinché specifichi un tipo di contenuto "application/json"e passi il tuo oggetto JSON serializzato nel flusso di input della tua richiesta (Nota, poiché questo flusso di inputèun flusso di memoria, va bene lasciarlo non disponibile, come memoria stream non mantiene alcuna risorsa esterna):

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;
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.