Como simular ModelState.IsValid usando a estrutura Moq?


88

Estou verificando ModelState.IsValidem meu método de ação do controlador que cria um funcionário como este:

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

    // Etc.
}

Eu quero simular isso em meu método de teste de unidade usando o Moq Framework. Eu tentei zombar assim:

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

Mas isso lança uma exceção no meu caso de teste de unidade. Alguém pode me ajudar aqui?

Respostas:


140

Você não precisa zombar disso. Se você já tem um controlador, pode adicionar um erro de estado do modelo ao inicializar o teste:

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

como definimos o ModelState.IsValid para acertar o caso verdadeiro? ModelState não tem um setter e, portanto, não podemos fazer o seguinte: _controllerUnderTest.ModelState.IsValid = true. Sem isso, não atingirá o funcionário
— Karan

4
@Newton, é verdade por padrão. Você não precisa especificar nada para acertar o caso verdadeiro. Se você quiser atingir o caso falso, basta adicionar um erro modelstate conforme mostrado na minha resposta.
— Darin Dimitrov

A melhor solução IMHO é usar o transportador mvc. Desta forma, você obtém um comportamento mais realista do seu controlador, você deve entregar a validação do modelo para o seu destino - validações de atributos. A postagem abaixo está descrevendo isso ( stackoverflow.com/a/5580363/572612 )
— Vladimir Shmidt

13

O único problema que tenho com a solução acima é que ela não testa o modelo se eu definir atributos. Eu configurei meu controlador desta forma.

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

O objeto modelBinder é o objeto que testa a validade do modelo. Dessa forma, posso apenas definir os valores do objeto e testá-lo.


1
Muito bom, isso é exatamente o que eu estava procurando. Eu não sei quantas pessoas postaram para uma pergunta antiga como essa, mas você teve valor para mim. Obrigado.
— W.Jackson

Parece uma ótima solução, ainda em 2016 :)
— Matt,

2
Não é melhor testar o modelo isoladamente com algo assim? stackoverflow.com/a/4331964/3198973
— RubberDuck

2
Embora seja uma solução inteligente, concordo com @RubberDuck. Para que este seja um teste de unidade real e isolado, a validação do modelo deve ser seu próprio teste, enquanto o teste do controlador deve ter seus próprios testes. Se o modelo mudar para violar a validação do ModelBinder, o teste do controlador falhará, o que é um falso positivo, pois a lógica do controlador não foi quebrada. Para testar um ModelStateDictionary inválido, basta adicionar um erro falso de ModelState para que a verificação ModelState.IsValid falhe.
— xDaevax

2

A resposta do uadrive me atrapalhou, mas ainda havia algumas lacunas. Sem nenhum dado na entrada para new NameValueCollectionValueProvider(), o fichário do modelo vinculará o controlador a um modelo vazio, não ao modelobjeto.

Tudo bem - basta serializar seu modelo como um NameValueCollectione, em seguida, passá-lo para o NameValueCollectionValueProviderconstrutor. Bem, não exatamente. Infelizmente, não funcionou no meu caso porque meu modelo contém uma coleção, e NameValueCollectionValueProvidernão combina bem com coleções.

O JsonValueProviderFactoryvem ao resgate aqui, no entanto. Ele pode ser usado pelo DefaultModelBinder, contanto que você especifique um tipo de conteúdo de "application/json"e passe seu objeto JSON serializado para o fluxo de entrada de sua solicitação (observe, porque esse fluxo de entrada é um fluxo de memória, não há problema em deixá-lo sem disposição, como uma memória stream não mantém nenhum recurso externo):

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;
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.