Pregunta Pruebas unitarias en la validación de MVC


¿Cómo puedo probar que la acción de mi controlador está poniendo los errores correctos en el estado del modelo al validar una entidad, cuando estoy usando la validación de la anotación de datos en MVC 2 Preview 1?

Un poco de código para ilustrar. Primero, la acción:

    [HttpPost]
    public ActionResult Index(BlogPost b)
    {
        if(ModelState.IsValid)
        {
            _blogService.Insert(b);
            return(View("Success", b));
        }
        return View(b);
    }

Y aquí hay una prueba de unidad fallida que creo que debería pasar, pero no lo es (utilizando MbUnit y Moq):

[Test]
public void When_processing_invalid_post_HomeControllerModelState_should_have_at_least_one_error()
{
    // arrange
    var mockRepository = new Mock<IBlogPostSVC>();
    var homeController = new HomeController(mockRepository.Object);

    // act
    var p = new BlogPost { Title = "test" };            // date and content should be required
    homeController.Index(p);

    // assert
    Assert.IsTrue(!homeController.ModelState.IsValid);
}

Supongo que además de esta pregunta, debería Probaré la validación, ¿y debería probarlo de esta manera?


75
2017-08-13 02:20


origen


Respuestas:


En lugar de pasar en un BlogPost también puedes declarar el parámetro de acciones como FormCollection. Entonces puedes crear el BlogPost usted mismo y llame UpdateModel(model, formCollection.ToValueProvider());.

Esto activará la validación de cualquier campo en el FormCollection.

    [HttpPost]
    public ActionResult Index(FormCollection form)
    {
        var b = new BlogPost();
        TryUpdateModel(model, form.ToValueProvider());

        if (ModelState.IsValid)
        {
            _blogService.Insert(b);
            return (View("Success", b));
        }
        return View(b);
    }

Solo asegúrese de que su prueba agregue un valor nulo para cada campo en el formulario de vistas que desea dejar vacío.

Descubrí que hacerlo de esta manera, a expensas de unas pocas líneas adicionales de código, hace que mis pruebas unitarias se asemejen a la forma en que se llama el código en tiempo de ejecución más de cerca, lo que las hace más valiosas. También puede probar qué sucede cuando alguien ingresa "abc" en un control vinculado a una propiedad int.


-3
2017-08-13 07:32



Odio necro una publicación anterior, pero pensé en agregar mis propios pensamientos (ya que acabo de tener este problema y encontré este post mientras buscaba la respuesta).

  1. No pruebe la validación en las pruebas de su controlador. O confía en la validación de MVC o escribe la suya (es decir, no prueba el código de otros, prueba su código)
  2. Si desea probar que la validación está haciendo lo que espera, pruébela en las pruebas de su modelo (esto lo hago para un par de mis validaciones de expresiones regulares más complejas).

Lo que realmente quiere probar aquí es que su controlador hace lo que espera que haga cuando la validación falla. Ese es tu código y tus expectativas. Probarlo es fácil una vez que se da cuenta de que eso es todo lo que quiere probar:

[test]
public void TestInvalidPostBehavior()
{
    // arrange
    var mockRepository = new Mock<IBlogPostSVC>();
    var homeController = new HomeController(mockRepository.Object);
    var p = new BlogPost();

    homeController.ViewData.ModelState.AddModelError("Key", "ErrorMessage"); // Values of these two strings don't matter.  
    // What I'm doing is setting up the situation: my controller is receiving an invalid model.

    // act
    var result = (ViewResult) homeController.Index(p);

    // assert
    result.ForView("Index")
    Assert.That(result.ViewData.Model, Is.EqualTo(p));
}

189
2017-09-28 19:02



He estado teniendo el mismo problema, y ​​después de leer la respuesta y el comentario de Pauls, busqué una forma de validar manualmente el modelo de vista.

encontré este tutorial que explica cómo validar manualmente un ViewModel que usa DataAnnotations. El fragmento de código Key está hacia el final de la publicación.

Modifiqué el código ligeramente: en el tutorial se omite el 4º parámetro del TryValidateObject (validateAllProperties). Para obtener todas las anotaciones para Validar, esto debe establecerse en verdadero.

Adicionalmente reestructuré el código en un método genérico, para hacer que la validación de ViewModel sea simple:

    public static void ValidateViewModel<TViewModel, TController>(this TController controller, TViewModel viewModelToValidate) 
        where TController : ApiController
    {
        var validationContext = new ValidationContext(viewModelToValidate, null, null);
        var validationResults = new List<ValidationResult>();
        Validator.TryValidateObject(viewModelToValidate, validationContext, validationResults, true);
        foreach (var validationResult in validationResults)
        {
            controller.ModelState.AddModelError(validationResult.MemberNames.FirstOrDefault() ?? string.Empty, validationResult.ErrorMessage);
        }
    }

Hasta ahora, esto ha funcionado muy bien para nosotros.


83
2017-07-28 13:08



Cuando llama al método homeController.Index en su prueba, no está utilizando ninguno de los framework MVC que desactive la validación para que ModelState.IsValid siempre sea verdadero. En nuestro código, llamamos un método Validate auxiliar directamente en el controlador en lugar de utilizar la validación ambiental. No he tenido mucha experiencia con las anotaciones de datos (utilizamos NHibernate.Validators), tal vez otra persona pueda ofrecerle orientación sobre cómo llamar a Validate desde su controlador.


6
2017-08-13 03:58



Estaba investigando esto hoy y encontré esta publicación en el blog por Roberto Hernández (MVP) que parece proporcionar la mejor solución para disparar los validadores para una acción de controlador durante la prueba unitaria. Esto colocará los errores correctos en el modelo de estado al validar una entidad.


3
2017-10-05 03:45



Estoy usando ModelBinders en mis casos de prueba para poder actualizar el valor de model.IsValid.

var form = new FormCollection();
form.Add("Name", "0123456789012345678901234567890123456789");

var model = MvcModelBinder.BindModel<AddItemModel>(controller, form);

ViewResult result = (ViewResult)controller.Add(model);

Con mi método MvcModelBinder.BindModel de la siguiente manera (básicamente el mismo código utilizado) internamente en el marco de MVC):

        public static TModel BindModel<TModel>(Controller controller, IValueProvider valueProvider) where TModel : class
        {
            IModelBinder binder = ModelBinders.Binders.GetBinder(typeof(TModel));
            ModelBindingContext bindingContext = new ModelBindingContext()
            {
                FallbackToEmptyPrefix = true,
                ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, typeof(TModel)),
                ModelName = "NotUsedButNotNull",
                ModelState = controller.ModelState,
                PropertyFilter = (name => { return true; }),
                ValueProvider = valueProvider
            };

            return (TModel)binder.BindModel(controller.ControllerContext, bindingContext);
        }

2
2018-02-18 19:50



Esto no responde exactamente a su pregunta, porque abandona DataAnnotations, pero la agregaré porque podría ayudar a otras personas a escribir pruebas para sus Controladores:

Tiene la opción de no utilizar la validación proporcionada por System.ComponentModel.DataAnnotations, pero aún utilizando el objeto ViewData.ModelState, utilizando su AddModelError método y algún otro mecanismo de validación. P.ej:

public ActionResult Create(CompetitionEntry competitionEntry)
{        
    if (competitionEntry.Email == null)
        ViewData.ModelState.AddModelError("CompetitionEntry.Email", "Please enter your e-mail");

    if (ModelState.IsValid)
    {
       // insert code to save data here...
       // ...

       return Redirect("/");
    }
    else
    {
        // return with errors
        var viewModel = new CompetitionEntryViewModel();
        // insert code to populate viewmodel here ...
        // ...


        return View(viewModel);
    }
}

Esto aún te permite aprovechar el Html.ValidationMessageFor() cosas que MVC genera, sin usar el DataAnnotations. Debe asegurarse de que la clave que usa con AddModelError coincide con lo que espera la vista para los mensajes de validación.

Luego, el controlador se puede probar porque la validación está ocurriendo explícitamente, en lugar de que el marco MVC lo haga de forma automática.


1
2017-09-23 19:52



Estoy de acuerdo en que ARM tiene la mejor respuesta: prueba el comportamiento de tu controlador, no la validación incorporada.

Sin embargo, también puede probar por unidad que su modelo / modelo de vista tiene definidos los atributos de validación correctos. Digamos que su ViewModel se ve así:

public class PersonViewModel
{
    [Required]
    public string FirstName { get; set; }
}

Esta unidad de prueba evaluará la existencia de [Required] atributo:

[TestMethod]
public void FirstName_should_be_required()
{
    var propertyInfo = typeof(PersonViewModel).GetProperty("FirstName");

    var attribute = propertyInfo.GetCustomAttributes(typeof(RequiredAttribute), false)
                                .FirstOrDefault();

    Assert.IsNotNull(attribute);
}

1
2018-05-26 16:41



A diferencia de ARM, no tengo ningún problema con la excavación de tumbas. Así que aquí está mi sugerencia. Se basa en la respuesta de Giles Smith y funciona para ASP.NET MVC4 (sé que la pregunta es sobre MVC 2, pero Google no discrimina cuando busca respuestas y no puedo probar en MVC2). En lugar de poner el código de validación en un método estático genérico, lo puse en un controlador de prueba. El controlador tiene todo lo necesario para la validación. Entonces, el controlador de prueba se ve así:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Wbe.Mvc;

protected class TestController : Controller
    {
        public void TestValidateModel(object Model)
        {
            ValidationContext validationContext = new ValidationContext(Model, null, null);
            List<ValidationResult> validationResults = new List<ValidationResult>();
            Validator.TryValidateObject(Model, validationContext, validationResults, true);
            foreach (ValidationResult validationResult in validationResults)
            {
                this.ModelState.AddModelError(String.Join(", ", validationResult.MemberNames), validationResult.ErrorMessage);
            }
        }
    }

Por supuesto, la clase no necesita ser una clase interna protegida, así es como la uso ahora, pero probablemente voy a reutilizar esa clase. Si en alguna parte hay un modelo MyModel que está decorado con agradables atributos de anotación de datos, entonces la prueba se ve más o menos así:

    [TestMethod()]
    public void ValidationTest()
    {
        MyModel item = new MyModel();
        item.Description = "This is a unit test";
        item.LocationId = 1;

        TestController testController = new TestController();
        testController.TestValidateModel(item);

        Assert.IsTrue(testController.ModelState.IsValid, "A valid model is recognized.");
    }

La ventaja de esta configuración es que puedo reutilizar el controlador de prueba para las pruebas de todos mis modelos y es posible que pueda ampliarlo para que se burle un poco más del controlador o use los métodos protegidos que tiene un controlador.

Espero eso ayude.


1
2018-03-21 20:38