Existe algum valor real no teste de unidade de um controlador no ASP.NET MVC?

33

Espero que esta pergunta dê algumas respostas interessantes porque é uma que me incomoda por um tempo.

Existe algum valor real no teste de unidade de um controlador no ASP.NET MVC?

O que quero dizer com isso é que, na maioria das vezes, (e eu não sou um gênio), meus métodos de controle são, mesmo nos mais complexos, algo como isto:

public ActionResult Create(MyModel model)
{
    // start error list
    var errors = new List<string>();

    // check model state based on data annotations
    if(ModelState.IsValid)
    {
        // call a service method
        if(this._myService.CreateNew(model, Request.UserHostAddress, ref errors))
        {
            // all is well, data is saved, 
            // so tell the user they are brilliant
            return View("_Success");
        }
    }

    // add errors to model state
    errors.ForEach(e => ModelState.AddModelError("", e));

    // return view
    return View(model);
}

A maior parte do trabalho pesado é feita pelo pipeline MVC ou pela minha biblioteca de serviços.

Então, talvez as perguntas a serem feitas sejam:

  • qual seria o valor da unidade testando esse método?
  • não seria interrompido Request.UserHostAddresse ModelStatecom uma NullReferenceException? Devo tentar zombar deles?
  • se eu refatorar esse método em um "auxiliar" reutilizável (o que eu provavelmente deveria, considerando quantas vezes o faço!), testaria isso mesmo que vale a pena quando tudo o que realmente estou testando é principalmente o "pipeline" que, presumivelmente, foi testado para uma polegada de sua vida pela Microsoft?

Eu acho que realmente quero dizer , fazer o seguinte parece totalmente inútil e errado

[TestMethod]
public void Test_Home_Index()
{
    var controller = new HomeController();
    var expected = "Index";
    var actual = ((ViewResult)controller.Index()).ViewName;
    Assert.AreEqual(expected, actual);
}

Obviamente, estou sendo obtuso com este exemplo exageradamente inútil, mas alguém tem alguma sabedoria a acrescentar aqui?

Ansioso por isso ... Obrigado.

LiverpoolsNumber9
fonte
Eu acho que o ROI (retorno do investimento) nesse teste em particular não vale o esforço, a menos que você tenha tempo e dinheiro infinitos. Eu escreveria testes que Kevin aponta para verificar as coisas com maior probabilidade de quebrar ou que o ajudarão a refatorar algo com confiança ou garantir que a propagação de erros ocorra conforme o esperado. Testes de pipeline, se necessário, podem ser feitos em um nível mais global / de infraestrutura e em métodos individuais, terão pouco valor. Não estou dizendo que eles não têm valor, mas "pouco". Portanto, se ele fornecer um bom ROI no seu caso, vá em frente, ou então pegue o peixe maior primeiro!
Mrchief

Respostas:

18

Mesmo para algo tão simples, um teste de unidade servirá a vários propósitos

  1. Confiança, o que foi escrito está em conformidade com a saída esperada. Pode parecer trivial verificar se ele retorna a visão correta, mas o resultado é uma evidência objetiva de que o requisito foi atendido
  2. Teste de regressão. Se o método Create precisar mudar, você ainda tem um teste de unidade para a saída esperada. Sim, a saída pode mudar e isso resulta em um teste quebradiço, mas ainda é uma verificação contra o controle de alterações não gerenciadas

Para essa ação específica, eu testaria o seguinte

  1. O que acontece se _myService for nulo?
  2. O que acontece se _myService.Create lançar uma exceção, ela lança uma específica para lidar?
  3. Um _myService.Create bem-sucedido retorna a exibição _Success?
  4. Os erros são propagados até o ModelState?

Você apontou para verificar Request and Model for NullReferenceException e acho que o ModelState.IsValid cuidará do tratamento de NullReference for Model.

Zombar da solicitação permite que você se proteja contra uma solicitação nula, que geralmente é impossível na produção, mas que pode ocorrer em um teste de unidade. Em um Teste de Integração, permitiria fornecer valores diferentes de UserHostAddress (uma solicitação ainda é entrada do usuário no que diz respeito ao controle e deve ser testada de acordo)

Kevin
fonte
Olá Kevin, obrigado por reservar um tempo para responder. Vou demorar um pouco para ver se mais alguém entra com alguma coisa, mas até agora a sua é a mais lógica / clara.
LiverpoolsNumber9
Spifty. Ainda bem que ajudou você.
21413 Kevin
3

Meus controladores também são muito pequenos. A maior parte da "lógica" nos controladores é manipulada usando atributos de filtro (interno e escrito à mão). Portanto, meu controlador geralmente possui apenas alguns trabalhos:

  • Crie modelos a partir de strings de consulta HTTP, valores de formulário etc.
  • Execute alguma validação básica
  • Ligar para meus dados ou camada de negócios
  • Gere um ActionResult

A maior parte da ligação do modelo é feita automaticamente pelo ASP.NET MVC. As DataAnnotations também lidam com a maior parte da validação.

Mesmo com tão pouco a testar, eu ainda os escrevo normalmente. Basicamente, testo se meus repositórios são chamados e se o ActionResulttipo correto é retornado. Eu tenho um método de conveniência ViewResultpara garantir que o caminho de exibição correto seja retornado e o modelo de exibição tenha a aparência esperada. Eu tenho outro para verificar se o controlador / ação correto está definido RedirectToActionResult. Eu tenho outros testes para JsonResult, etc. etc.

Um resultado infeliz da subclasse da Controllerclasse é que ela fornece muitos métodos de conveniência que usam HttpContextinternamente. Isso dificulta o teste da unidade do controlador. Por esse motivo, normalmente coloco HttpContextchamadas dependentes atrás de uma interface e passo essa interface para o construtor do controlador (eu uso a extensão da web Ninject para criar meus controladores para mim). Essa interface geralmente é onde colo as propriedades auxiliares para acessar a sessão, as definições de configuração, os auxiliares de IPrinciple e URL.

Isso requer muita diligência, mas acho que vale a pena.

Travis Parks
fonte
Agradecemos o tempo necessário para responder, mas duas questões imediatamente. Em primeiro lugar, os "métodos auxiliares" nos testes de unidade são v. Perigosos. Em segundo lugar, "teste que meus repositórios são chamados" - você quer dizer via injeção de dependência?
LiverpoolsNumber9
Por que métodos de conveniência seriam perigosos? Eu tenho uma BaseControllerTestsaula onde todos moram. Eu zombei dos meus repositórios. Eu os conecto usando o Ninject.
Travis Parks
O que acontece se você cometeu um erro ou uma suposição incorreta em seus ajudantes? Meu outro argumento era que apenas um teste de integração (ou seja, de ponta a ponta) poderia "testar" se seus repositórios são chamados. Em um teste de unidade, você "atualizaria" ou zombaria de seus repositórios manualmente de qualquer maneira.
LiverpoolsNumber9
Você passa o repositório para o construtor. Você zomba durante o teste. Você garante que a simulação seja executada conforme o esperado. Os ajudantes simplesmente desconstruir ActionResulté para inspecionar URLs passados, modelos, etc.
Parques Travis
Ok, é justo - eu entendi um pouco o que você quis dizer com "teste de que meus repositórios são chamados".
LiverpoolsNumber9
2

Obviamente, alguns controladores são muito mais complexos que isso, mas baseados puramente no seu exemplo:

O que acontece se o myService lançar uma exceção?

Como uma nota rodapé.

Além disso, eu questionaria a sabedoria de passar uma lista por referência (não é necessário, pois o c # passa por referência de qualquer maneira, mas mesmo que não fosse) - passar uma ação errorAction (Action) que o serviço pode usar para bombear mensagens de erro para que pode ser manipulado da maneira que você deseja (talvez você queira adicioná-lo à lista, talvez você queira adicionar um erro de modelo, talvez queira registrá-lo).

No seu exemplo:

em vez de erros de ref, faça (string s) => ModelState.AddModelError ("", s), por exemplo.

Michael
fonte
Vale ressaltar, isso pressupõe que seu serviço esteja residindo no mesmo aplicativo, caso contrário, os problemas de serialização entrarão em jogo.
Michael
O serviço estaria em uma dll separada. Mas de qualquer maneira, você provavelmente está certo sobre o "ref". No seu outro ponto, não importa se o myService lança uma exceção. Não estou testando o myService - testaria os métodos nele separadamente. Estou falando de testar puramente a "unidade" do ActionResult com (provavelmente) um myService zombado.
LiverpoolsNumber9
Você tem um mapeamento 1: 1 entre seu serviço e seu controlador? Caso contrário, alguns controladores usam várias chamadas de serviço? Se sim, você poderia testar essas interações?
Michael
Não. No final do dia, os métodos de serviço recebem entrada (geralmente um modelo de exibição ou apenas strings / ints), eles "fazem coisas" e, em seguida, retornam um erro / erro se falso. Não há link "direto" entre os controladores e a camada de serviço. Os estão completamente separados.
LiverpoolsNumber9
Sim, eu entendo isso, estou tentando entender o modelo relacional entre os controladores e a camada de serviço - supondo que cada controlador não tenha um método de serviço correspondente, seria lógico que alguns controladores precisem fazer uso de mais de um método de serviço?
Michael