¿Cómo simular ModelState.IsValid usando el marco Moq?

91

Estoy comprobando el ModelState.IsValidmétodo de acción de mi controlador que crea un empleado como este:

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

    // Etc.
}

Quiero simularlo en mi método de prueba unitaria usando Moq Framework. Traté de burlarme de esto así:

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

Pero esto arroja una excepción en mi caso de prueba unitaria. ¿Puede alguien ayudarme aquí?

Mazen
fuente

Respuestas:

142

No necesitas burlarte de eso. Si ya tiene un controlador, puede agregar un error de estado del modelo al inicializar su prueba:

// 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
fuente
¿Cómo configuramos ModelState.IsValid para que se adapte al caso verdadero? ModelState no tiene un establecedor y, por lo tanto, no podemos hacer lo siguiente: _controllerUnderTest.ModelState.IsValid = true. Sin eso, no afectará al empleado
Karan
4
@Newton, es cierto por defecto. No es necesario especificar nada para dar con el caso real. Si desea acertar en el caso falso, simplemente agregue un error de estado del modelo como se muestra en mi respuesta.
Darin Dimitrov
En mi humilde opinión, una mejor solución es utilizar un transportador de PVC. De esta manera, obtiene un comportamiento más realista de su controlador, debe entregar la validación del modelo a su destino: validaciones de atributos. La siguiente publicación describe esto ( stackoverflow.com/a/5580363/572612 )
Vladimir Shmidt
13

El único problema que tengo con la solución anterior es que en realidad no prueba el modelo si configuro atributos. Configuré mi controlador de esta manera.

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

El objeto modelBinder es el objeto que prueba la validez del modelo. De esta manera, puedo establecer los valores del objeto y probarlo.

uadrive
fuente
1
Muy bonito, esto es exactamente lo que estaba buscando. No sé cuántas personas publican una pregunta antigua como esta, pero para mí te valió. Gracias.
W.Jackson
Parece una gran solución, todavía en 2016 :)
Matt
2
¿No es mejor probar el modelo de forma aislada con algo como esto? stackoverflow.com/a/4331964/3198973
RubberDuck
2
Si bien esta es una solución inteligente, estoy de acuerdo con @RubberDuck. Para que se trate de una prueba unitaria real y aislada, la validación del modelo debe ser su propia prueba, mientras que la prueba del controlador debe tener sus propias pruebas. Si el modelo cambia para violar la validación de ModelBinder, la prueba de su controlador fallará, lo cual es un falso positivo ya que la lógica del controlador no está rota. Para probar un ModelStateDictionary no válido, simplemente agregue un error ModelState falso para que la verificación ModelState.IsValid falle.
xDaevax
2

La respuesta de uadrive me llevó parte del camino, pero aún quedaban algunas lagunas. Sin ningún dato en la entrada new NameValueCollectionValueProvider(), la carpeta de modelos vinculará el controlador a un modelo vacío, no al modelobjeto.

Está bien, simplemente serialice su modelo como NameValueCollectiony luego páselo al NameValueCollectionValueProviderconstructor. Bueno, no del todo. Desafortunadamente, no funcionó en mi caso porque mi modelo contiene una colección y NameValueCollectionValueProviderno funciona bien con las colecciones.

Sin JsonValueProviderFactoryembargo, viene al rescate aquí. Puede ser utilizado por el DefaultModelBindersiempre que especifique un tipo de contenido de "application/json"y pase su objeto JSON serializado al flujo de entrada de su solicitud (tenga en cuenta que, debido a que este flujo de entrada es un flujo de memoria, está bien dejarlo sin eliminar, como una memoria stream no se aferra a ningún 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
fuente