Compartilhar via


Habilitar o teste de unidade automatizado

pela Microsoft

Baixar PDF

Esta é a etapa 12 de um tutorial gratuito de aplicativo "NerdDinner" que explica como criar um aplicativo Web pequeno, mas completo, usando ASP.NET MVC 1.

A etapa 12 mostra como desenvolver um conjunto de testes de unidade automatizados que verificam nossa funcionalidade NerdDinner e que nos dará confiança para fazer alterações e melhorias no aplicativo no futuro.

Se você estiver usando ASP.NET MVC 3, recomendamos que siga os tutoriais Introdução With MVC 3 ou MVC Music Store.

NerdDinner Etapa 12: Teste de Unidade

Vamos desenvolver um conjunto de testes de unidade automatizados que verificam nossa funcionalidade NerdDinner e que nos dará confiança para fazer alterações e melhorias no aplicativo no futuro.

Por que teste de unidade?

No caminho para o trabalho uma manhã você tem um flash repentino de inspiração sobre um aplicativo em que você está trabalhando. Você percebe que há uma alteração que você pode implementar que tornará o aplicativo dramaticamente melhor. Pode ser uma refatoração que limpa o código, adiciona um novo recurso ou corrige um bug.

A pergunta que o confronta quando você chega ao seu computador é : "quão seguro é fazer essa melhoria?" E se fazer a alteração tiver efeitos colaterais ou quebrar algo? A alteração pode ser simples e levar apenas alguns minutos para ser implementada, mas e se levar horas para testar manualmente todos os cenários do aplicativo? E se você esquecer de cobrir um cenário e um aplicativo quebrado entrar em produção? Fazer essa melhoria realmente vale todo o esforço?

Os testes de unidade automatizados podem fornecer uma rede de segurança que permite aprimorar continuamente seus aplicativos e evitar ter medo do código no qual você está trabalhando. Ter testes automatizados que verificam rapidamente a funcionalidade permite codificar com confiança e capacitá-lo a fazer melhorias que talvez você não tenha se sentido confortável fazendo. Eles também ajudam a criar soluções que são mais mantenedíveis e têm um tempo de vida mais longo - o que leva a um retorno muito maior sobre o investimento.

O ASP.NET MVC Framework torna fácil e natural a funcionalidade do aplicativo de teste de unidade. Ele também habilita um fluxo de trabalho de TDD (Desenvolvimento Controlado por Teste) que permite o desenvolvimento baseado em teste.

Projeto NerdDinner.Tests

Quando criamos nosso aplicativo NerdDinner no início deste tutorial, fomos solicitados com uma caixa de diálogo perguntando se queríamos criar um projeto de teste de unidade para acompanhar o projeto de aplicativo:

Captura de tela da caixa de diálogo Criar Projeto de Teste de Unidade. Sim, criar um projeto de teste de unidade está selecionado. Nerd Dinner dot Tests é escrito como o nome do projeto de teste.

Mantivemos o botão de opção "Sim, criar um projeto de teste de unidade" selecionado – o que resultou na adição de um projeto "NerdDinner.Tests" à nossa solução:

Captura de tela da árvore de navegação Gerenciador de Soluções. Nerd Dinner dot Tests está selecionado.

O projeto NerdDinner.Tests faz referência ao assembly do projeto de aplicativo NerdDinner e nos permite adicionar facilmente testes automatizados a ele que verificam a funcionalidade do aplicativo.

Criando testes de unidade para nossa classe de modelo de jantar

Vamos adicionar alguns testes ao nosso projeto NerdDinner.Tests que verificam a classe Dinner que criamos quando criamos nossa camada de modelo.

Começaremos criando uma nova pasta em nosso projeto de teste chamada "Modelos", em que colocaremos nossos testes relacionados ao modelo. Em seguida, clicaremos com o botão direito do mouse na pasta e escolheremos o comando de menu Adicionar> Novo Teste . Isso abrirá a caixa de diálogo "Adicionar Novo Teste".

Vamos optar por criar um "Teste de Unidade" e nomeá-lo como "DinnerTest.cs":

Captura de tela da caixa de diálogo Adicionar Novo Teste. O Teste de Unidade está realçado. Dinner Test dot c s é escrito como o Nome do Teste.

Quando clicarmos no botão "ok", o Visual Studio adicionará (e abrirá) um arquivo DinnerTest.cs ao projeto:

Captura de tela do arquivo do ponto cs do Dinner Test no Visual Studio.

O modelo de teste de unidade padrão do Visual Studio tem um monte de código clichê dentro dele que eu acho um pouco confuso. Vamos limpo para apenas conter o código abaixo:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NerdDinner.Models;

namespace NerdDinner.Tests.Models {
 
    [TestClass]
    public class DinnerTest {

    }
}

O atributo [TestClass] na classe DinnerTest acima o identifica como uma classe que conterá testes, bem como a inicialização de teste opcional e o código de desativação. Podemos definir testes dentro dele adicionando métodos públicos que têm um atributo [TestMethod] neles.

Abaixo estão os primeiros de dois testes que adicionaremos ao exercício nossa aula de jantar. O primeiro teste verifica se nosso Jantar é inválido se um novo Jantar é criado sem que todas as propriedades sejam definidas corretamente. O segundo teste verifica se nosso Jantar é válido quando um Jantar tem todas as propriedades definidas com valores válidos:

[TestClass]
public class DinnerTest {

    [TestMethod]
    public void Dinner_Should_Not_Be_Valid_When_Some_Properties_Incorrect() {

        //Arrange
        Dinner dinner = new Dinner() {
            Title = "Test title",
            Country = "USA",
            ContactPhone = "BOGUS"
        };

        // Act
        bool isValid = dinner.IsValid;

        //Assert
        Assert.IsFalse(isValid);
    }

    [TestMethod]
    public void Dinner_Should_Be_Valid_When_All_Properties_Correct() {
        
        //Arrange
        Dinner dinner = new Dinner {
            Title = "Test title",
            Description = "Some description",
            EventDate = DateTime.Now,
            HostedBy = "ScottGu",
            Address = "One Microsoft Way",
            Country = "USA",
            ContactPhone = "425-703-8072",
            Latitude = 93,
            Longitude = -92,
        };

        // Act
        bool isValid = dinner.IsValid;

        //Assert
        Assert.IsTrue(isValid);
    }
}

Você observará acima que nossos nomes de teste são muito explícitos (e um pouco detalhados). Estamos fazendo isso porque podemos acabar criando centenas ou milhares de testes pequenos, e queremos facilitar a determinação rápida da intenção e do comportamento de cada um deles (especialmente quando estamos examinando uma lista de falhas em um executor de teste). Os nomes de teste devem ser nomeados após a funcionalidade que estão testando. Acima, estamos usando um padrão de nomenclatura "Noun_Should_Verb".

Estamos estruturando os testes usando o padrão de teste "AAA", que significa "Organizar, Agir, Afirmar":

  • Organizar: configurar a unidade que está sendo testada
  • Agir: exerça a unidade em resultados de teste e captura
  • Assert: verificar o comportamento

Quando escrevemos testes, queremos evitar que os testes individuais façam muito. Em vez disso, cada teste deve verificar apenas um único conceito (o que facilitará muito a identificação da causa das falhas). Uma boa diretriz é tentar ter apenas uma única instrução de declaração para cada teste. Se você tiver mais de uma instrução assert em um método de teste, verifique se todos eles estão sendo usados para testar o mesmo conceito. Quando estiver em dúvida, faça outro teste.

Executando testes

O Visual Studio 2008 Professional (e edições superiores) inclui um executor de teste interno que pode ser usado para executar projetos de Teste de Unidade do Visual Studio no IDE. Podemos selecionar o comando de menu Test-Run-All>> Tests in Solution (ou digite Ctrl R, A) para executar todos os nossos testes de unidade. Ou, como alternativa, podemos posicionar nosso cursor dentro de uma classe de teste ou método de teste específico e usar o comando de menu Test-Run-Tests>> no Contexto Atual (ou digite Ctrl R, T) para executar um subconjunto dos testes de unidade.

Vamos posicionar nosso cursor dentro da classe DinnerTest e digitar "Ctrl R, T" para executar os dois testes que acabamos de definir. Quando fizermos isso, uma janela "Resultados do Teste" será exibida no Visual Studio e veremos os resultados de nossa execução de teste listados nele:

Captura de tela da janela Resultados do Teste no Visual Studio. Os resultados da execução do teste são listados dentro.

Observação: a janela de resultados do teste do VS não mostra a coluna Nome da Classe por padrão. Você pode adicioná-lo clicando com o botão direito do mouse na janela Resultados do Teste e usando o comando de menu Adicionar/Remover Colunas.

Nossos dois testes levaram apenas uma fração de segundo para serem executados – e como você pode ver, ambos passaram. Agora podemos continuar e aumentá-los criando testes adicionais que verificam validações de regra específicas, bem como abrangem os dois métodos auxiliares – IsUserHost() e IsUserRegistered() – que adicionamos à classe Dinner. Ter todos esses testes em vigor para a classe Dinner tornará muito mais fácil e seguro adicionar novas regras de negócios e validações a ela no futuro. Podemos adicionar nossa nova lógica de regra ao Dinner e, em segundos, verificar se ela não quebrou nenhuma das nossas funcionalidades lógicas anteriores.

Observe como usar um nome de teste descritivo facilita a compreensão rápida do que cada teste está verificando. É recomendável usar o comando de menu Ferramentas-Opções>, abrir a tela de configuração de Execução Ferramentas de Teste-Teste> e verificar a caixa de seleção "Clicar duas vezes em um resultado de teste de unidade com falha ou inconclusiva exibe o ponto de falha no teste". Isso permitirá que você clique duas vezes em uma falha na janela de resultados do teste e pule imediatamente para a falha de declaração.

Criando jantares Testes de unidade doControlador

Agora vamos criar alguns testes de unidade que verificam nossa funcionalidade DinnersController. Começaremos clicando com o botão direito do mouse na pasta "Controladores" em nosso projeto de teste e, em seguida, escolheremos o comando de menu Adicionar> Novo Teste . Criaremos um "Teste de Unidade" e o nomearemos como "DinnersControllerTest.cs".

Criaremos dois métodos de teste que verificam o método de ação Details() no DinnersController. O primeiro verificará se um Modo de Exibição é retornado quando um jantar existente é solicitado. A segunda verificará se uma exibição "NotFound" é retornada quando um Jantar inexistente é solicitado:

[TestClass]
public class DinnersControllerTest {

    [TestMethod]
    public void DetailsAction_Should_Return_View_For_ExistingDinner() {

        // Arrange
        var controller = new DinnersController();

        // Act
        var result = controller.Details(1) as ViewResult;

        // Assert
        Assert.IsNotNull(result, "Expected View");
    }

    [TestMethod]
    public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {

        // Arrange
        var controller = new DinnersController();

        // Act
        var result = controller.Details(999) as ViewResult;

        // Assert
        Assert.AreEqual("NotFound", result.ViewName);
    } 
}

O código acima compila limpo. No entanto, quando executamos os testes, ambos falham:

Captura de tela do código. Ambos os testes falharam.

Se examinarmos as mensagens de erro, veremos que o motivo pelo qual os testes falharam foi porque nossa classe DinnersRepository não pôde se conectar a um banco de dados. Nosso aplicativo NerdDinner está usando uma cadeia de conexão para um arquivo de SQL Server Express local que reside no diretório \App_Data do projeto de aplicativo NerdDinner. Como nosso projeto NerdDinner.Tests compila e é executado em um diretório diferente, o projeto do aplicativo, o local do caminho relativo da cadeia de conexão está incorreto.

Podemos corrigir isso copiando o arquivo de banco de dados do SQL Express para nosso projeto de teste e, em seguida, adicionar uma cadeia de conexão de teste apropriada a ele no App.config do nosso projeto de teste. Isso obteria os testes acima desbloqueados e em execução.

O código de teste de unidade usando um banco de dados real, no entanto, traz uma série de desafios. Especificamente:

  • Ele reduz significativamente o tempo de execução dos testes de unidade. Quanto mais tempo demorar para executar testes, menor a probabilidade de executá-los com frequência. O ideal é que os testes de unidade possam ser executados em segundos e que isso seja algo que você faça tão naturalmente quanto compilar o projeto.
  • Isso complica a lógica de instalação e limpeza nos testes. Você deseja que cada teste de unidade seja isolado e independente de outros (sem efeitos colaterais ou dependências). Ao trabalhar em um banco de dados real, você precisa estar atento ao estado e redefini-lo entre testes.

Vamos examinar um padrão de design chamado "injeção de dependência" que pode nos ajudar a contornar esses problemas e evitar a necessidade de usar um banco de dados real com nossos testes.

Injeção de dependência

Agora DinnersController está fortemente "acoplado" à classe DinnerRepository. "Acoplamento" refere-se a uma situação em que uma classe depende explicitamente de outra classe para funcionar:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    //
    // GET: /Dinners/Details/5

    public ActionResult Details(int id) {

        Dinner dinner = dinnerRepository.FindDinner(id);

        if (dinner == null)
            return View("NotFound");

        return View(dinner);
    }

Como a classe DinnerRepository requer acesso a um banco de dados, a dependência firmemente acoplada que a classe DinnersController tem no DinnerRepository acaba exigindo que tenhamos um banco de dados para que os métodos de ação DinnersController sejam testados.

Podemos contornar isso empregando um padrão de design chamado "injeção de dependência", que é uma abordagem em que as dependências (como classes de repositório que fornecem acesso a dados) não são mais criadas implicitamente dentro de classes que as usam. Em vez disso, as dependências podem ser passadas explicitamente para a classe que as usa usando argumentos de construtor. Se as dependências forem definidas usando interfaces, teremos a flexibilidade de passar implementações de dependência "falsas" para cenários de teste de unidade. Isso nos permite criar implementações de dependência específicas de teste que não exigem acesso a um banco de dados.

Para ver isso em ação, vamos implementar a injeção de dependência com nosso DinnersController.

Extraindo uma interface IDinnerRepository

Nossa primeira etapa será criar uma nova interface IDinnerRepository que encapsula o contrato de repositório que nossos controladores exigem para recuperar e atualizar o Dinners.

Podemos definir esse contrato de interface manualmente clicando com o botão direito do mouse na pasta \Models e escolhendo o comando de menu Adicionar> Novo Item e criando uma nova interface chamada IDinnerRepository.cs.

Como alternativa, podemos usar as ferramentas de refatoração internas Visual Studio Professional (e edições superiores) para extrair e criar automaticamente uma interface para nós de nossa classe DinnerRepository existente. Para extrair essa interface usando o VS, basta posicionar o cursor no editor de texto na classe DinnerRepository e clicar com o botão direito do mouse e escolher o comando de menu Refactor-Extract> Interface :

Captura de tela que mostra a opção Extrair Interface selecionada no submenu Refatorar.

Isso iniciará a caixa de diálogo "Extrair Interface" e nos solicitará o nome da interface a ser criada. O padrão será IDinnerRepository e selecionará automaticamente todos os métodos públicos na classe DinnerRepository existente para adicionar à interface:

Captura de tela da janela Resultados do Teste no Visual Studio.

Quando clicarmos no botão "ok", o Visual Studio adicionará uma nova interface IDinnerRepository ao nosso aplicativo:

public interface IDinnerRepository {

    IQueryable<Dinner> FindAllDinners();
    IQueryable<Dinner> FindByLocation(float latitude, float longitude);
    IQueryable<Dinner> FindUpcomingDinners();
    Dinner             GetDinner(int id);

    void Add(Dinner dinner);
    void Delete(Dinner dinner);
    
    void Save();
}

E nossa classe DinnerRepository existente será atualizada para que ela implemente a interface :

public class DinnerRepository : IDinnerRepository {
   ...
}

Atualizando DinnersController para dar suporte à injeção de construtor

Agora atualizaremos a classe DinnersController para usar a nova interface.

Atualmente DinnersController é embutido em código, de modo que seu campo "dinnerRepository" é sempre uma classe DinnerRepository:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    ...
}

Vamos alterá-lo para que o campo "dinnerRepository" seja do tipo IDinnerRepository em vez de DinnerRepository. Em seguida, adicionaremos dois construtores DinnersController públicos. Um dos construtores permite que um IDinnerRepository seja passado como um argumento. O outro é um construtor padrão que usa nossa implementação DinnerRepository existente:

public class DinnersController : Controller {

    IDinnerRepository dinnerRepository;

    public DinnersController()
        : this(new DinnerRepository()) {
    }

    public DinnersController(IDinnerRepository repository) {
        dinnerRepository = repository;
    }
    ...
}

Como ASP.NET MVC, por padrão, cria classes de controlador usando construtores padrão, nosso DinnersController em runtime continuará a usar a classe DinnerRepository para executar o acesso a dados.

No entanto, agora podemos atualizar nossos testes de unidade para passar em uma implementação de repositório de jantar "falsa" usando o construtor de parâmetros. Esse repositório de jantar "falso" não exigirá acesso a um banco de dados real e, em vez disso, usará dados de exemplo na memória.

Criando a classe FakeDinnerRepository

Vamos criar uma classe FakeDinnerRepository.

Começaremos criando um diretório "Fakes" em nosso projeto NerdDinner.Tests e, em seguida, adicionaremos uma nova classe FakeDinnerRepository a ele (clique com o botão direito do mouse na pasta e escolha Adicionar> Nova Classe):

Captura de tela do item de menu Adicionar Nova Classe. Adicionar Novo Item está realçado.

Atualizaremos o código para que a classe FakeDinnerRepository implemente a interface IDinnerRepository. Em seguida, podemos clicar com o botão direito do mouse nele e escolher o comando de menu de contexto "Implementar interface IDinnerRepository":

Captura de tela do comando de menu de contexto Implementar interface I Dinner Repository.

Isso fará com que o Visual Studio adicione automaticamente todos os membros da interface IDinnerRepository à nossa classe FakeDinnerRepository com implementações padrão de "stub out":

public class FakeDinnerRepository : IDinnerRepository {

    public IQueryable<Dinner> FindAllDinners() {
        throw new NotImplementedException();
    }

    public IQueryable<Dinner> FindByLocation(float lat, float long){
        throw new NotImplementedException();
    }

    public IQueryable<Dinner> FindUpcomingDinners() {
        throw new NotImplementedException();
    }

    public Dinner GetDinner(int id) {
        throw new NotImplementedException();
    }

    public void Add(Dinner dinner) {
        throw new NotImplementedException();
    }

    public void Delete(Dinner dinner) {
        throw new NotImplementedException();
    }

    public void Save() {
        throw new NotImplementedException();
    }
}

Em seguida, podemos atualizar a implementação de FakeDinnerRepository para trabalhar fora de uma coleção List<Dinner> na memória passada para ela como um argumento de construtor:

public class FakeDinnerRepository : IDinnerRepository {

    private List<Dinner> dinnerList;

    public FakeDinnerRepository(List<Dinner> dinners) {
        dinnerList = dinners;
    }

    public IQueryable<Dinner> FindAllDinners() {
        return dinnerList.AsQueryable();
    }

    public IQueryable<Dinner> FindUpcomingDinners() {
        return (from dinner in dinnerList
                where dinner.EventDate > DateTime.Now
                select dinner).AsQueryable();
    }

    public IQueryable<Dinner> FindByLocation(float lat, float lon) {
        return (from dinner in dinnerList
                where dinner.Latitude == lat && dinner.Longitude == lon
                select dinner).AsQueryable();
    }

    public Dinner GetDinner(int id) {
        return dinnerList.SingleOrDefault(d => d.DinnerID == id);
    }

    public void Add(Dinner dinner) {
        dinnerList.Add(dinner);
    }

    public void Delete(Dinner dinner) {
        dinnerList.Remove(dinner);
    }

    public void Save() {
        foreach (Dinner dinner in dinnerList) {
            if (!dinner.IsValid)
                throw new ApplicationException("Rule violations");
        }
    }
}

Agora temos uma implementação IDinnerRepository falsa que não requer um banco de dados e, em vez disso, podemos trabalhar com uma lista na memória de objetos Dinner.

Usando o FakeDinnerRepository com testes de unidade

Vamos retornar aos testes de unidade DinnersController que falharam anteriormente porque o banco de dados não estava disponível. Podemos atualizar os métodos de teste para usar um FakeDinnerRepository preenchido com dados de jantar em memória de exemplo para o DinnersController usando o código abaixo:

[TestClass]
public class DinnersControllerTest {

    List<Dinner> CreateTestDinners() {

        List<Dinner> dinners = new List<Dinner>();

        for (int i = 0; i < 101; i++) {

            Dinner sampleDinner = new Dinner() {
                DinnerID = i,
                Title = "Sample Dinner",
                HostedBy = "SomeUser",
                Address = "Some Address",
                Country = "USA",
                ContactPhone = "425-555-1212",
                Description = "Some description",
                EventDate = DateTime.Now.AddDays(i),
                Latitude = 99,
                Longitude = -99
            };
            
            dinners.Add(sampleDinner);
        }
        
        return dinners;
    }

    DinnersController CreateDinnersController() {
        var repository = new FakeDinnerRepository(CreateTestDinners());
        return new DinnersController(repository);
    }

    [TestMethod]
    public void DetailsAction_Should_Return_View_For_Dinner() {

        // Arrange
        var controller = CreateDinnersController();

        // Act
        var result = controller.Details(1);

        // Assert
        Assert.IsInstanceOfType(result, typeof(ViewResult));
    }

    [TestMethod]
    public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {

        // Arrange
        var controller = CreateDinnersController();

        // Act
        var result = controller.Details(999) as ViewResult;

        // Assert
        Assert.AreEqual("NotFound", result.ViewName);
    }
}

E agora, quando executamos esses testes, ambos são aprovados:

Captura de tela dos testes de unidade, ambos os testes foram aprovados.

O melhor de tudo é que eles levam apenas uma fração de segundo para serem executados e não exigem nenhuma lógica complicada de instalação/limpeza. Agora podemos testar por unidade todo o nosso código de método de ação DinnersController (incluindo listagem, paginação, detalhes, criar, atualizar e excluir) sem precisar se conectar a um banco de dados real.

Tópico lateral: estruturas de injeção de dependência
Executar injeção de dependência manual (como estamos acima) funciona bem, mas fica mais difícil de manter à medida que o número de dependências e componentes em um aplicativo aumenta. Existem várias estruturas de injeção de dependência para .NET que podem ajudar a fornecer ainda mais flexibilidade de gerenciamento de dependências. Essas estruturas, também às vezes chamadas de contêineres de "Inversão de Controle" (IoC), fornecem mecanismos que permitem um nível adicional de suporte de configuração para especificar e passar dependências para objetos em runtime (geralmente usando injeção de construtor). Algumas das estruturas mais populares de Injeção de Dependência de OSS /IOC no .NET incluem: AutoFac, Ninject, Spring.NET, StructureMap e Windsor. ASP.NET MVC expõe APIs de extensibilidade que permitem que os desenvolvedores participem da resolução e instanciação de controladores e que permitem que as estruturas de Injeção de Dependência/IoC sejam totalmente integradas nesse processo. Usar uma estrutura DI/IOC também nos permitiria remover o construtor padrão de nosso DinnersController , o que removeria completamente o acoplamento entre ele e o DinnerRepository. Não usaremos uma estrutura de injeção de dependência/IOC com nosso aplicativo NerdDinner. Mas é algo que poderíamos considerar para o futuro se a base de código nerdDinner e as capacidades crescesse.

Criando editar testes de unidade de ação

Agora vamos criar alguns testes de unidade que verificam a funcionalidade Editar do DinnersController. Começaremos testando a versão HTTP-GET de nossa ação Editar:

//
// GET: /Dinners/Edit/5

[Authorize]
public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (!dinner.IsHostedBy(User.Identity.Name))
        return View("InvalidOwner");

    return View(new DinnerFormViewModel(dinner));
}

Criaremos um teste que verifica se um View apoiado por um objeto DinnerFormViewModel é renderizado novamente quando um jantar válido é solicitado:

[TestMethod]
public void EditAction_Should_Return_View_For_ValidDinner() {

    // Arrange
    var controller = CreateDinnersController();

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}

No entanto, quando executarmos o teste, descobriremos que ele falha porque uma exceção de referência nula é gerada quando o método Edit acessa a propriedade User.Identity.Name para executar a marcar Dinner.IsHostedBy().

O objeto User na classe base Controller encapsula detalhes sobre o usuário conectado e é preenchido por ASP.NET MVC quando ele cria o controlador em runtime. Como estamos testando o DinnersController fora de um ambiente de servidor Web, o objeto User não está definido (portanto, a exceção de referência nula).

Zombando da propriedade User.Identity.Name

Simular estruturas facilita o teste, permitindo que criemos dinamicamente versões falsas de objetos dependentes que dão suporte a nossos testes. Por exemplo, podemos usar uma estrutura de simulação em nosso teste de ação Editar para criar dinamicamente um objeto User que nosso DinnersController pode usar para pesquisar um nome de usuário simulado. Isso evitará que uma referência nula seja gerada quando executarmos nosso teste.

Há muitas estruturas de simulação do .NET que podem ser usadas com ASP.NET MVC (você pode ver uma lista delas aqui: http://www.mockframeworks.com/).

Depois de baixado, adicionaremos uma referência em nosso projeto NerdDinner.Tests ao assembly Moq.dll:

Captura de tela da árvore de navegação Nerd Dinner. Moq está realçado.

Em seguida, adicionaremos um método auxiliar "CreateDinnersControllerAs(username)" à nossa classe de teste que usa um nome de usuário como parâmetro e que, em seguida, "simula" a propriedade User.Identity.Name na instância dinnersController:

DinnersController CreateDinnersControllerAs(string userName) {

    var mock = new Mock<ControllerContext>();
    mock.SetupGet(p => p.HttpContext.User.Identity.Name).Returns(userName);
    mock.SetupGet(p => p.HttpContext.Request.IsAuthenticated).Returns(true);

    var controller = CreateDinnersController();
    controller.ControllerContext = mock.Object;

    return controller;
}

Acima, estamos usando o Moq para criar um objeto Mock que falsifica um objeto ControllerContext (que é o que ASP.NET MVC passa para classes Controller para expor objetos de runtime como Usuário, Solicitação, Resposta e Sessão). Estamos chamando o método "SetupGet" no Mock para indicar que a propriedade HttpContext.User.Identity.Name em ControllerContext deve retornar a cadeia de caracteres de nome de usuário que passamos para o método auxiliar.

Podemos simular qualquer número de propriedades e métodos ControllerContext. Para ilustrar isso, também adicionei uma chamada SetupGet() para a propriedade Request.IsAuthenticated (que não é realmente necessária para os testes abaixo, mas que ajuda a ilustrar como você pode simular as propriedades de Solicitação). Quando terminarmos, atribuimos uma instância da simulação ControllerContext ao DinnersController que nosso método auxiliar retorna.

Agora podemos escrever testes de unidade que usam esse método auxiliar para testar cenários de Edição envolvendo diferentes usuários:

[TestMethod]
public void EditAction_Should_Return_EditView_When_ValidOwner() {

    // Arrange
    var controller = CreateDinnersControllerAs("SomeUser");

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}

[TestMethod]
public void EditAction_Should_Return_InvalidOwnerView_When_InvalidOwner() {

    // Arrange
    var controller = CreateDinnersControllerAs("NotOwnerUser");

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.AreEqual(result.ViewName, "InvalidOwner");
}

E agora, quando executamos os testes, eles são aprovados:

Captura de tela dos testes de unidade que usam o método auxiliar. Os testes foram aprovados.

Testando cenários UpdateModel()

Criamos testes que abrangem a versão HTTP-GET da ação Editar. Agora vamos criar alguns testes que verificam a versão HTTP-POST da ação Editar:

//
// POST: /Dinners/Edit/5

[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Edit (int id, FormCollection collection) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (!dinner.IsHostedBy(User.Identity.Name))
        return View("InvalidOwner");

    try {
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
        ModelState.AddModelErrors(dinner.GetRuleViolations());
        
        return View(new DinnerFormViewModel(dinner));
    }
}

O novo cenário de teste interessante para darmos suporte a esse método de ação é o uso do método auxiliar UpdateModel() na classe base Controller. Estamos usando esse método auxiliar para associar valores de postagem de formulário à nossa instância de objeto Dinner.

Abaixo estão dois testes que demonstram como podemos fornecer valores postados de formulário para o método auxiliar UpdateModel() a ser usado. Faremos isso criando e populando um objeto FormCollection e atribuindo-o à propriedade "ValueProvider" no Controlador.

O primeiro teste verifica se, em um salvamento bem-sucedido, o navegador é redirecionado para a ação de detalhes. O segundo teste verifica se quando a entrada inválida é postada, a ação exibe novamente o modo de exibição de edição com uma mensagem de erro.

[TestMethod]
public void EditAction_Should_Redirect_When_Update_Successful() {

    // Arrange      
    var controller = CreateDinnersControllerAs("SomeUser");

    var formValues = new FormCollection() {
        { "Title", "Another value" },
        { "Description", "Another description" }
    };

    controller.ValueProvider = formValues.ToValueProvider();
    
    // Act
    var result = controller.Edit(1, formValues) as RedirectToRouteResult;

    // Assert
    Assert.AreEqual("Details", result.RouteValues["Action"]);
}

[TestMethod]
public void EditAction_Should_Redisplay_With_Errors_When_Update_Fails() {

    // Arrange
    var controller = CreateDinnersControllerAs("SomeUser");

    var formValues = new FormCollection() {
        { "EventDate", "Bogus date value!!!"}
    };

    controller.ValueProvider = formValues.ToValueProvider();

    // Act
    var result = controller.Edit(1, formValues) as ViewResult;

    // Assert
    Assert.IsNotNull(result, "Expected redisplay of view");
    Assert.IsTrue(result.ViewData.ModelState.Count > 0, "Expected errors");
}

Testando Wrap-Up

Abordamos os principais conceitos envolvidos em classes de controlador de teste de unidade. Podemos usar essas técnicas para criar facilmente centenas de testes simples que verificam o comportamento do nosso aplicativo.

Como nossos testes de controlador e modelo não exigem um banco de dados real, eles são extremamente rápidos e fáceis de executar. Poderemos executar centenas de testes automatizados em segundos e imediatamente receber comentários sobre se uma alteração que fizemos quebrou algo. Isso nos ajudará a fornecer confiança para melhorar, refatorar e refinar continuamente nosso aplicativo.

Abordamos o teste como o último tópico deste capítulo, mas não porque o teste é algo que você deve fazer no final de um processo de desenvolvimento! Pelo contrário, você deve escrever testes automatizados o mais cedo possível em seu processo de desenvolvimento. Isso permite que você receba comentários imediatos à medida que desenvolve, ajuda você a pensar cuidadosamente sobre os cenários de caso de uso do seu aplicativo e orienta você a projetar seu aplicativo com limpo camadas e acoplamento em mente.

Um capítulo posterior do livro discutirá o TDD (Desenvolvimento Controlado por Teste) e como usá-lo com ASP.NET MVC. O TDD é uma prática de codificação iterativa em que você primeiro escreve os testes que seu código resultante atenderá. Com o TDD, você começa cada recurso criando um teste que verifica a funcionalidade que você está prestes a implementar. Escrever o teste de unidade primeiro ajuda a garantir que você entenda claramente o recurso e como ele deve funcionar. Somente depois que o teste é gravado (e você verificou que ele falha) você implementa a funcionalidade real que o teste verifica. Como você já passou um tempo pensando no caso de uso de como o recurso deve funcionar, você terá uma melhor compreensão dos requisitos e da melhor maneira de implementá-los. Quando terminar a implementação, você poderá executar novamente o teste e obter comentários imediatos sobre se o recurso funciona corretamente. Abordaremos mais o TDD no Capítulo 10.

Próxima etapa

Alguns comentários finais.