Testes unitários com Visual Studio

Testes unitários com Visual Studio

Existem várias ferramentas de teste unitário para .NET, muitos dos quais são de código aberto e disponível gratuitamente, um exemplo bem conhecido é o NUnit. Mas neste post vamos usar o suporte de teste unitário built-in do Visual Studio, que possui como grande vantagem sua integração com a IDE, que torna mais de configurar e executar os testes sem a necessidade de ferramentas externas.

Microsoft Visual Web Developer Express não inclui suporte para testes unitários. Esta é uma das formas que a Microsoft diferencia as versões gratuitas e comerciais do Visual Studio. Se Você estiver utilizando o Web Developer Express, é recomendável que você utilize o NUnit(www.nunit.org), que funciona de forma semelhando ao suporte built-in do Visual Studio.

Criando o projeto

Vamos criar um projeto console para demonstrar os teste unitários. Crie o projeto usando o template console application e defina o nome dele como ProdutoApp. Após você criar o projeto defina as interfaces e o modelo como abaixo:


public class Produto
{
    public int ProdutoId { get; set; }
    public string Nome { get; set; }
    public string Descricao { get; set; }
    public decimal Preco { get; set; }
    public string Categoria { set; get; }
}

interface IProdutoRepositorio
{
    IEnumerable RetornaProdutos();
    void AtualizaProduto(Produto produto);
}

interface IRedutorPreco
{
    void ReduzirPrecos(decimal precoReducao);
}

A classe Produto é uma entidade do nosso modelo com seus atributos específicos. A interface IProdutoRepositorio define um repositório através do qual nos vamos obter e atualizar os objetos Produto. A interface IRedutorPreco especifica uma função que irá ser aplicada a todos os produtos, reduzindo seu preço pela quantidade especificada no parâmetro precoReducao.

Nosso objetivo é a criação de uma implementação de IRedutorPreco que atenda os seguintes requisitos:

  • O preço de todos os itens no repositório deve ser reduzido.
  • A redução total deve ser o valor do parâmetro precoReducao multiplicado pelo número total de produtos.
  • O método AtualizaProduto no repositório deve ser chamado para cada objeto Produto.
  • Nenhum preço deve ser reduzido para menos de R$1.

Para nos ajudar na implementação, criamos a classe FakeRepositorio, que implementa a interface IProdutoRepositorio, como é mostrado abaixo:


public class FakeRepositorio:IProdutoRepositorio
{
    private Produto[] produtos = {
                                    new Produto(){Nome = "Caiaque",Preco = 275M},
                                    new Produto(){Nome = "Colete salva-vidas",Preco = 275M},
                                    new Produto(){Nome = "Bola de futebol",Preco = 275M},
                                    new Produto(){Nome = "Chuteira",Preco = 275M},
                                 };

public int AtualizeContagemDeChamadasProduto { get; set; }

public IEnumerable<Produto> RetornaProdutos()
{
    return produtos;
}

public void AtualizaProduto(Produto produtoParemetro)
{
    foreach (var produto in produtos.Where(p=>p.Nome==produtoParemetro.Nome).Select(p=>p))
    {
        produto.Preco = produtoParemetro.Preco;
    }
    AtualizeContagemDeChamadasProduto++;
}

public decimal RetornaValorTotal()
{
    return produtos.Sum(e => e.Preco);
}
}

Voltaremos a essa classe posteriormente. Vamos escrever também o esqueleto da classe MeuRedutorPreco, que será a nossa implementação da interface IRedutorPreco, como mostrado abaixo:


public class MeuRedutorPreco:IRedutorPreco
{
    private IProdutoRepositorio _repositorio;
 
    public MeuRedutorPreco(IProdutoRepositorio repo)
    {
        _repositorio = repo;
    }
 
    public void ReduzirPrecos(decimal precoReducao)
    {
        throw new NotImplementedException();
    }
}

Essa classe ainda não implementa o método ReduzirPrecos, mas tem um construtor que vai nos deixar injetar uma implementação da interface IProdutoRepositorio.

O ultimo passo é adicionar Ninject como uma referencia ao nosso projeto, usando Library Package Manager ou uma versão que você baixou do site da Ninject.

 

Criando os Testes Unitários

Vamos seguir o padrão TDD e escrever nossos testes unitários antes de escrever o código do aplicativo. Botão direito do mouse no método MeuRedutorPreco.ReduzirPrecos e selecione Create Unit Tests…

O Visual Studio irá exibir a janela Create Unit Tests. Todos os tipos que estão disponíveis no projeto são exibidos, e você pode conferir para os quais os teste devem ser criados. Como começamos esse processos a partir do método ReduzirPrecos na classe  MeuRedutorPreco, esse item já marcado.

Os testes unitários são criados em um projeto separado a partir do próprio aplicativo. Uma vez que você ainda não criou um projeto como este, a opção Project Output está configurado para criar um projeto novo para nós. Clique no botão OK, e o Visual Studio irá solicitar um nome para o projeto de teste. A convenção que sigo é nomear o projeto <MainProjectName>.Tests. Uma vez que nosso projeto é chamado ProdutoApp, nosso projeto de teste será chamado ProdutoApp.Tests.

Clique no botão Criar para criar o projeto e teste unitário. O Visual Studio irá adicionar o projeto à solução existente. Se você abrir o item referências para o projeto de teste na janela Solution Explorer, você verá que o Visual Studio adicionou automaticamente as referências de assembly que precisa, incluindo o output do projeto principal e o Ninject.

Um novo arquivo é adicionado chamado MeuRedutorPrecoTest.cs ele contém algumas propriedades e métodos para nos ajudar a começar. No entanto, vamos ignorá-los e começar do zero. Edite o arquivo para que fique como o abaixo:



[TestClass()]
public class MeuRedutorPrecoTest
{
    [TestMethod()]

    public void Deve_Trocar_Todos_os_Precos()
    {
    // Arrange
    FakeRepositorio repo = new FakeRepositorio();
    decimal valorReducao = 10;
    IEnumerable<decimal> precos = repo.RetornaProdutos().Select(e => e.Preco);
    decimal[] precosIniciais = precos.ToArray();

    MeuRedutorPreco alvo = new MeuRedutorPreco(repo);

    // Act
    alvo.ReduzirPrecos(valorReducao);
    precos.Zip(precosIniciais, (p1, p2) =>
    {
        if (p1 == p2)
        {
            Assert.Fail();
        }
        return p1;
        });
    }
}

O código acima contem o primeiro dos nossos testes unitários e os atributos que o Visual Studio procura quando os testes são executados. O atributo TestClass é aplicado a uma classe que contém testes, e o atributo TestMethod é aplicado a qualquer método que contém um teste unitário. Métodos que não possuem este atributo são considerados métodos de apoio e são ignorados pelo Visual Studio.

Você pode ver que segui o padrão arrange/act/assert (A/A/A) no método de teste. Existe um grande número de convenções sobre como nomear testes unitários, mas a orientação geral é que você simplesmente use nomes que tornem claros o que o teste esta verificando.

No método Deve_Trocar_Todos_os_Precos retornamos todos os preços através de uma consulta LINQ em nosso FakeRepositorio de produtos, em seguida utilizamos o método ToArray() para armazenar os preços iniciais em um array chamado precosIniciais. Em seguida chamamos o método alvo e usamos o método LINQ ZIP para nos certificar que cada preço mudou. Se algum elemento não for alterado, chamamos o método Asset.Fail que faz nosso teste unitário falhar.

Há muitas maneiras diferentes de criar testes unitários. Um inconveniente comum é criar um único método gigante que testa todas as condições necessárias. Uma boa pratica é criar vários testes unitários pequenos onde cada um se concentra em determinado aspecto da aplicação.

Seguindo o padrão TDD nos continuamos criando nossos testes.


[TestMethod]
public void Deve_Verificar_Se_o_Valor_Total_de_Reducao_Esta_Correto_()
{
            // Arrange
            FakeRepositorio repo = new FakeRepositorio();
            decimal valorReducao = 10;
            decimal totalInicial = repo.RetornaValorTotal();
            MeuRedutorPreco alvo = new MeuRedutorPreco(repo);
 
            // Act
            alvo.ReduzirPrecos(valorReducao);
 
            // Assert
            Assert.AreEqual(repo.RetornaValorTotal(), (totalInicial - (repo.RetornaProdutos().Count() * valorReducao)));
        }
 
        [TestMethod]
        public void Nenhum_Preco_Inferior_a_Um_Real()
        {
            // Arrange
            FakeRepositorio repo = new FakeRepositorio();
            decimal valorReducao = decimal.MaxValue;
            MeuRedutorPreco alvo = new MeuRedutorPreco(repo);
 
            // Act
            alvo.ReduzirPrecos(valorReducao);
 
            // Assert
            foreach (Produto prod in repo.RetornaProdutos())
            {
                Assert.IsTrue(prod.Preco >= 1);
            }
}

Cada um destes métodos segue o mesmo padrão. Criamos um objeto FakeRepositorio e injetamos manualmente no construtor da classe MeuRedutorPreco. Em seguida, chamamos o método ReduzirPrecos e verificamos os resultados, utilizando os métodos da classe Assert. Os testes acima estão bem simples, por isso não vou entrar em detalhes de cada um.

Executando os testes unitários (e falhando)

Para executar esses testes selecione Run no menu Test do Visual Studio, e selecione All Tests in Solution. O Visual Studio ira percorrer todas as classes na solution atual procurando os atributos TestClass e TestMethod.

A janela Test Results exibe o progresso de cada teste é realizado, e dá um indicador verde ou vermelho para mostrar os resultados. Ainda temos que implementar a nossa funcionalidade em MeuRedutorPreco.ReduzirPrecos que esta retornando uma NotImplementedException, então todos os nossos testes unitários irão falhar.

Implementando a Feature

Chegamos ao ponto em que podemos implementar o recurso, com a certeza de que seremos capazes de verificar a qualidade do nosso código quando finalizamos nossa implementação. O método ReduzirPrecos é bastante simples:



public class MeuRedutorPreco:IRedutorPreco
{
    private IProdutoRepositorio _repositorio;

public MeuRedutorPreco(IProdutoRepositorio repo)
{
    _repositorio = repo;
}

public void ReduzirPrecos(decimal precoReducao)
{
    foreach (Produto p in _repositorio.RetornaProdutos())
    {
        p.Preco = Math.Max(p.Preco - precoReducao, 1);
        _repositorio.AtualizaProduto(p);
    }
}

Agora ao rodar nossos testes novamente, todos eles passam:

Essa foi apenas uma introdução muito rápida nos testes unitários, mas foi possível observar que o Visual Studio possui funcionalidades avançadas para você tirar proveito. Recomendo que você explore mais sobre testes unitários na documentação do MSDN.

Referências:

Pro ASP.NET MVC 3 Framework, Third Edition Copyright © 2011 by Adam Freeman and Steven Sanderson