Comment se moquer de ModelState.IsValid en utilisant le framework Moq?

88

Je vérifie ModelState.IsValidma méthode d'action de contrôleur qui crée un employé comme ceci:

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

    // Etc.
}

Je veux me moquer de cela dans ma méthode de test unitaire en utilisant Moq Framework. J'ai essayé de me moquer de ça comme ça:

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

Mais cela jette une exception dans mon cas de test unitaire. Est-ce que quelqu'un pourrait m'aider?

Mazen
la source

Réponses:

140

Vous n'avez pas besoin de vous en moquer. Si vous disposez déjà d'un contrôleur, vous pouvez ajouter une erreur d'état du modèle lors de l'initialisation de votre 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();
Darin Dimitrov
la source
comment définir le ModelState.IsValid pour qu'il atteigne le vrai cas? ModelState n'a pas de setter et nous ne pouvons donc pas effectuer les opérations suivantes: _controllerUnderTest.ModelState.IsValid = true. Sans cela, il ne touchera pas l'employé
Karan
4
@Newton, c'est vrai par défaut. Vous n'avez pas besoin de spécifier quoi que ce soit pour atteindre le vrai cas. Si vous voulez frapper le faux cas, vous ajoutez simplement une erreur modelstate comme indiqué dans ma réponse.
Darin Dimitrov
IMHO Une meilleure solution consiste à utiliser un convoyeur mvc. De cette façon, vous obtenez un comportement plus réaliste de votre contrôleur, vous devez fournir une validation de modèle à son destin - des validations d'attributs. Le post ci-dessous décrit cela ( stackoverflow.com/a/5580363/572612 )
Vladimir Shmidt
13

Le seul problème que j'ai avec la solution ci-dessus est qu'elle ne teste pas réellement le modèle si je définis des attributs. J'ai configuré mon contrôleur de cette façon.

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'objet modelBinder est l'objet qui teste la validité du modèle. De cette façon, je peux simplement définir les valeurs de l'objet et le tester.

uadrive
la source
1
Très joli, c'est exactement ce que je cherchais. Je ne sais pas combien de personnes postent sur une vieille question comme celle-ci, mais cela vous a valu de la valeur pour moi. Merci.
W.Jackson
Cela semble être une excellente solution, toujours en 2016 :)
Matt
2
N'est-il pas préférable de tester le modèle isolément avec quelque chose comme ça? stackoverflow.com/a/4331964/3198973
RubberDuck
2
Bien que ce soit une solution intelligente, je suis d'accord avec @RubberDuck. Pour qu'il s'agisse d'un test unitaire réel et isolé, la validation du modèle doit être son propre test, tandis que le test du contrôleur doit avoir ses propres tests. Si le modèle change pour violer la validation ModelBinder, votre test de contrôleur échouera, ce qui est un faux positif car la logique du contrôleur n'est pas interrompue. Pour tester un ModelStateDictionary non valide, ajoutez simplement une fausse erreur ModelState pour que la vérification ModelState.IsValid échoue.
xDaevax
2

La réponse d'uadrive m'a pris une partie du chemin, mais il y avait encore des lacunes. Sans aucune donnée dans l'entrée de new NameValueCollectionValueProvider(), le classeur de modèles liera le contrôleur à un modèle vide, pas à l' modelobjet.

C'est très bien - sérialisez simplement votre modèle en tant que NameValueCollection, puis transmettez-le au NameValueCollectionValueProviderconstructeur. Enfin, pas tout à fait. Malheureusement, cela n'a pas fonctionné dans mon cas car mon modèle contient une collection et NameValueCollectionValueProviderne joue pas bien avec les collections.

Le JsonValueProviderFactoryvient à la rescousse ici, cependant. Il peut être utilisé par le DefaultModelBindertant que vous spécifiez un type de contenu de "application/json"et que vous transmettez votre objet JSON sérialisé dans le flux d'entrée de votre requête (veuillez noter que ce flux d'entrée est un flux de mémoire, vous pouvez le laisser non disposé, en tant que mémoire stream ne conserve aucune ressource externe):

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
la source