Pruebas unitarias de validación MVC


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

Algún código para ilustrar. En primer lugar, 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 unitaria fallida que creo que debería estar pasando pero no lo está (usando 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 Estar probando la validación, y debería estar probando de esta manera?

Author: Marc Climent, 2009-08-13

12 answers

En lugar de pasar un BlogPost también puede declarar el parámetro actions como FormCollection. Luego puede crear el BlogPost usted mismo y llamar a UpdateModel(model, formCollection.ToValueProvider());.

Esto activará la validación para 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 null 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 parezcan a la forma en que se llama al código tiempo de ejecución más de cerca haciéndolos más valiosos. También puede probar lo que sucede cuando alguien ingresa " abc " en un control vinculado a una propiedad int.

 -3
Author: Maurice,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-05-18 14:08:00

Odio necro un viejo post, pero pensé que me gustaría añadir mis propios pensamientos (ya que acabo de tener este problema y me encontré con este post mientras buscaba la respuesta).

  1. No pruebe la validación en las pruebas del controlador. Ya sea que confíes en la validación de MVC o escribas la tuya propia (es decir, no pruebes el código de otros, prueba tu código)
  2. Si desea probar la validación está haciendo lo que espera, pruébelo en sus pruebas de modelo (hago esto para un par de mis expresiones regulares más complejas validación).

Lo que realmente quiere probar aquí es que su controlador hace lo que espera que haga cuando falla la validación. Ese es tu código, y tus expectativas. Probar es fácil una vez que te das cuenta de que eso es todo lo que quieres 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
Author: ARM,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-09-28 19:02:35

Yo había estado teniendo el mismo problema, y después de leer la respuesta y el comentario de Paul, busqué una forma de validar manualmente el modelo de vista.

Encontré este tutorial que explica cómo validar manualmente un ViewModel que usa DataAnnotations. Ellos Clave fragmento de código es hacia el final de la publicación.

He modificado ligeramente el código - en el tutorial se omite el 4to parámetro del TryValidateObject (validateAllProperties). Con el fin de obtener todas las anotaciones a Validar, esto debe establecerse en true.

Además me refactorizado el código en un método genérico, para hacer pruebas de ViewModel validación 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.

 84
Author: Giles Smith,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-03-30 20:45:50

Cuando llamas al HomeController.Método de índice en su prueba, no está utilizando ninguno de los marcos MVC que dispara la validación para ModelState.IsValid siempre será verdad. En nuestro código llamamos a un método helper Validate directamente en el controlador en lugar de usar la validación ambiental. No he tenido mucha experiencia con las anotaciones de datos (usamos NHibernate.Validadores) tal vez alguien más pueda ofrecer orientación sobre cómo llamar a Validate desde su controlador.

 6
Author: Paul Alexander,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-08-13 03:58:12

Estaba investigando esto hoy y encontré esta entrada de blog de Roberto Hernández (MVP) que parece proporcionar la mejor solución para disparar los validadores para una acción de controlador durante las pruebas unitarias. Esto pondrá los errores correctos en el ModelState al validar una entidad.

 3
Author: Darren,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-10-05 03:45:24

Estoy usando ModelBinders en mis casos de prueba para poder actualizar el modelo.Valor isValid.

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

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

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

Con mi MvcModelBinder.Método BindModel de la siguiente manera (básicamente el mismo código utilizado internamente en el marco 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
Author: ggarber,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-02-18 19:50:14

Esto no responde exactamente a tu pregunta, porque abandona las anotaciones de datos, pero lo 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 el Sistema.ComponentModel.DataAnnotations pero aún usando ViewData.Objeto ModelState, usando su método AddModelError y algún otro mecanismo de validación. Por ejemplo:

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 todavía le permite tomar ventaja de la Html.ValidationMessageFor() cosas que MVC genera, sin usando DataAnnotations. Debe asegurarse de que la clave que utiliza con AddModelError coincide con lo que la vista espera para los mensajes de validación.

El controlador entonces se vuelve comprobable porque la validación está sucediendo explícitamente, en lugar de ser realizada automáticamente por el framework MVC.

 1
Author: codeulike,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-09-24 08:46:54

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

Sin embargo, también puede probar unitariamente que su Modelo/ViewModel tiene definidos los atributos de validación correctos. Digamos que tu ViewModel se ve así:

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

Esta prueba unitaria probará la existencia del atributo [Required]:

[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
Author: Alex York,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-26 16:41:41

A diferencia de ARM, no tengo problemas con la excavación de tumbas. Así que aquí está mi sugerencia. Se basa en la respuesta de Giles Smith y trabaja 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. Por lo tanto, 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);
            }
        }
    }

De por supuesto, la clase no necesita ser una clase interna protegida, esa es la forma en que la uso ahora, pero probablemente voy a reutilizar esa clase. Si en algún lugar hay un modelo MyModel que está decorado con buenos atributos de anotación de datos, entonces la prueba se ve algo como esto:

    [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 puede ser capaz de extenderlo para burlarse un poco más sobre el controlador o utilizar los métodos protegidos que un controlador tener.

Espero que ayude.

 1
Author: Albert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-03-22 14:07:48

Si le importa la validación pero no le importa cómo se implementa, si solo le importa la validación de su método de acción en el nivel más alto de abstracción, no importa si se implementa como uso de DataAnnotations, ModelBinders o incluso ActionFilterAttributes, entonces podría usar Xania.AspNet.Paquete simulador nuget de la siguiente manera:

install-package Xania.AspNet.Simulator

--

var action = new BlogController()
    .Action(c => c.Index(new BlogPost()), "POST");
var modelState = action.ValidateRequest();

modelState.IsValid.Should().BeFalse();
 1
Author: Ibrahim ben Salah,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-01 22:04:32

Basado en la respuesta y comentarios de @giles-smith, para Web API:

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

Ver en la edición de respuestas arriba...

 0
Author: malix,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-01 21:56:40

La respuesta de@giles-smith es mi enfoque preferido, pero la implementación se puede simplificar:

    public static void ValidateViewModel(this Controller controller, object viewModelToValidate)
    {
        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);
        }
    }
 0
Author: Sam Shiles,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-09 08:42:24