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?

Mazen
fonte

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();
Darin Dimitrov
fonte
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.

uadrive
fonte
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;
}
Rob Lyndon
fonte