Pergunta

Como posso testar se minha ação do controlador está colocando os corrigir erros no ModelState ao validar uma entidade, quando estou usando validação DataAnnotation no MVC 2 Preview 1?

Alguns código para ilustrar. Primeiro, a ação:

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

E aqui está um teste de unidade falha que eu acho que deve estar passando, mas não é (usando MbUnit & 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);
}

Eu acho que, para além desta questão, deve eu estar testando validação, e que eu deveria estar testando-o desta forma?

Foi útil?

Solução

Em vez de passar em um BlogPost você também pode declarar as ações de parâmetros como FormCollection. Em seguida, você pode criar a si mesmo BlogPost e UpdateModel(model, formCollection.ToValueProvider()); chamada.

Isto irá acionar a validação para qualquer campo na 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);
    }

Apenas certifique-se o seu teste adiciona um valor nulo para todos os campos nas vistas forma que você quer deixar vazio.

Descobri que fazê-lo desta forma, à custa de algumas linhas extras de código, faz meus testes de unidade assemelham-se a forma como o código é chamado em tempo de execução mais de perto o que os torna mais valioso. Além disso, você pode testar o que acontece quando alguém entra "abc" em um controle ligado a uma propriedade int.

Outras dicas

Odeio necro um post antigo, mas eu pensei que eu gostaria de acrescentar meus próprios pensamentos (desde que eu apenas tive este problema e correu este post, enquanto procuram a resposta).

  1. Não validação de teste em seus testes de controlador. Ou você confia validação do MVC ou escreve sua própria (ou seja, não testar o código do outro, testar seu código)
  2. Se você quer validação do teste é fazer o que você espera, testá-lo em seus testes de modelo (eu faço isso por um par de minhas validações regex mais complexos).

O que você realmente quer testar aqui é que o controlador faz o que você espera que ele faça quando validação falha. Esse é o seu código e suas expectativas. Testá-lo é fácil uma vez que você percebe que é tudo que você quer de teste:

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

Eu estava tendo o mesmo problema, e depois de ler Pauls resposta e comentário, procurei uma forma de validar manualmente o modelo de vista.

Eu encontrei neste tutorial que explica como validar manualmente um ViewModel que usa DataAnnotations. Eles trecho de código chave é para o final do post.

I alterado o código ligeiramente - no tutorial do 4º parâmetro do TryValidateObject é omitido (validateAllProperties). A fim de obter todas as anotações para validar, isso deve ser definido como verdadeiro.

Adicionalmente eu refatorado o código em um método genérico, para fazer testes de ViewModel validação simples:

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

Até agora isso tem funcionado muito bem para nós.

Quando você chamar o método homeController.Index em seu teste, você não está usando qualquer um dos framework MVC que os incêndios fora da validação para ModelState.IsValid será sempre verdade. No nosso código que chamar um método Validar ajudante diretamente no controlador ao invés de usar a validação ambiente. Eu não tive muita experiência com os DataAnnotations (Usamos NHibernate.Validators) talvez alguém pode oferecer orientação como chamar Validar dentro de seu controlador.

Eu estava pesquisando sobre isso hoje e achei este post por Roberto Hernández (MVP), que parece fornecer a melhor solução para disparar os validadores para uma ação de controlador durante testes de unidade. Isto irá colocar os corrigir erros no ModelState ao validar uma entidade.

Eu estou usando ModelBinders em meus casos de teste para ser capaz de atualizar o valor model.IsValid.

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

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

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

Com o meu método MvcModelBinder.BindModel da seguinte forma (basicamente o mesmo código usado internamente no framework 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);
        }

Esta não responde exatamente a sua pergunta, porque ele abandona DataAnnotations, mas eu vou adicioná-lo porque ele pode ajudar outras pessoas a escrever testes para os seus controladores:

Você tem a opção de não usar a validação fornecido pela System.ComponentModel.DataAnnotations mas ainda usando o objeto ViewData.ModelState, usando seu método AddModelError e algum outro mecanismo de validação. Por exemplo:

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

Este ainda lhe permite tirar partido das coisas Html.ValidationMessageFor() que MVC gera, sem usar o DataAnnotations. Você tem que ter certeza a chave que você usar com partidas AddModelError o que a vista está esperando para mensagens de validação.

O controlador torna-se então testável porque a validação está acontecendo de forma explícita, em vez de ser feito automagicamente pelo framework MVC.

Eu concordo que a ARM tem a melhor resposta: testar o comportamento de seu controlador, não o built-in de validação

.

No entanto, você também pode teste de unidade que o seu modelo / ViewModel tem a validação correta atributos definidos. Vamos dizer que seu ViewModel esta aparência:

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

Este teste unidade irá testar a existência do 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);
}

Em contraste com a ARM, eu não tenho um problema com grave escavação. Então aqui está a minha sugestão. Baseia-se na resposta de Giles Smith e trabalha para ASP.NET MVC4 (eu sei que a pergunta é sobre MVC 2, mas o Google não discrimina quando à procura de respostas e eu não posso testar no MVC2.) Em vez de colocar o código de validação em um método estático genérico, eu colocá-lo em um controlador de teste. O controlador tem tudo o necessário para validação. Assim, os olhares controlador de teste como este:

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

É claro que a classe não precisa ser um innerclass protegido, que é a forma como eu usá-lo agora, mas eu provavelmente vou reutilizar essa classe. Se em algum lugar há uma MyModel modelo que está decorado com agradáveis ??atributos de anotação de dados, então o teste é algo como isto:

    [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.");
    }

A vantagem desta configuração é que posso reutilizar o controlador de teste para testes de todos os meus modelos e pode ser capaz de estendê-lo para zombar um pouco mais sobre o controlador ou usar os métodos protegidos que um controlador tem.

Hope isso ajuda.

Se você se preocupa com a validação, mas você não se preocupam com como ela é implementada, se você só se preocupam com a validação do seu método de ação no mais alto nível de abstração, não importa se ele é implementado como usar DataAnnotations, ModelBinders ou mesmo ActionFilterAttributes, então você pode usar o pacote NuGet Xania.AspNet.Simulator da seguinte forma:

install-package Xania.AspNet.Simulator

-

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

modelState.IsValid.Should().BeFalse();

Com base @ resposta e comentários, para Web API 's Giles-smith:

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

Veja na resposta editar acima ...

A resposta de

@ Giles-smith é a minha abordagem preferida mas a implementação pode ser simplificada:

    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);
        }
    }
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top