Passo a passo: Criar e executar testes de unidade para código gerenciado

Este artigo orienta você pela criação, execução e personalização de uma série de testes de unidade usando a estrutura de teste de unidade da Microsoft para código gerenciado e o Gerenciador de Testes do Visual Studio. Inicie com um projeto C# que está em desenvolvimento, crie testes que exercitem seu código, execute os testes e examine os resultados. Em seguida, você altera o código do projeto e executa os testes novamente. Se você quiser uma visão geral conceitual dessas tarefas antes de passar por essas etapas, consulte Noções básicas de teste de unidade.

Criar um projeto para teste

  1. Abra o Visual Studio.

  2. Na tela Iniciar, selecione Criar um novo projeto.

  3. Pesquise e selecione o modelo de projeto do Aplicativo de Console em C# para .NET e clique em Avançar.

    Observação

    Se não vir o modelo Aplicativo de Console, instale-o por meio da janela Criar um novo projeto. Na mensagem Não encontrou o que precisa?, escolha o link Instalar mais ferramentas e recursos. Em seguida, no Instalador do Visual Studio, escolha a carga de trabalho Desenvolvimento de área de trabalho do .NET.

  4. Dê ao projeto o nome Bank e clique em Avançar.

    Escolha a estrutura de destino recomendada ou o .NET 8 e escolha Criar.

    O projeto Bank é criado e exibido no Gerenciador de Soluções com o arquivo Program.cs aberto no editor de códigos.

    Observação

    Se Program.cs não estiver aberto no editor, clique duas vezes no arquivo Program.cs no Gerenciador de Soluções para abri-lo.

  5. Substitua o conteúdo do Program.cs pelo seguinte código C# que define uma classe, BankAccount:

    using System;
    
    namespace BankAccountNS
    {
        /// <summary>
        /// Bank account demo class.
        /// </summary>
        public class BankAccount
        {
            private readonly string m_customerName;
            private double m_balance;
    
            private BankAccount() { }
    
            public BankAccount(string customerName, double balance)
            {
                m_customerName = customerName;
                m_balance = balance;
            }
    
            public string CustomerName
            {
                get { return m_customerName; }
            }
    
            public double Balance
            {
                get { return m_balance; }
            }
    
            public void Debit(double amount)
            {
                if (amount > m_balance)
                {
                    throw new ArgumentOutOfRangeException("amount");
                }
    
                if (amount < 0)
                {
                    throw new ArgumentOutOfRangeException("amount");
                }
    
                m_balance += amount; // intentionally incorrect code
            }
    
            public void Credit(double amount)
            {
                if (amount < 0)
                {
                    throw new ArgumentOutOfRangeException("amount");
                }
    
                m_balance += amount;
            }
    
            public static void Main()
            {
                BankAccount ba = new BankAccount("Mr. Bryan Walton", 11.99);
    
                ba.Credit(5.77);
                ba.Debit(11.22);
                Console.WriteLine("Current balance is ${0}", ba.Balance);
            }
        }
    }
    
  6. Renomeie o arquivo como BankAccount.cs clicando com o botão direito do mouse e escolha Renomear na Gerenciador de Soluções.

  7. No menu Build clique em Criar solução (ou pressione Ctrl + SHIFT + B).

Agora você tem um projeto com métodos que você pode testar. Neste artigo, os testes se concentram no método Debit. O método Debit é chamado quando o dinheiro é retirado de uma conta.

Criar um projeto de teste de unidade

  1. No menu Arquivo, selecione Adicionar>Novo Projeto.

    Dica

    Você também pode clicar com o botão direito do mouse na solução no Gerenciador de Soluções e escolher Adicionar>Novo Projeto.

  2. Digite test na caixa de pesquisa, selecione C# como o idioma e, em seguida, selecione o Projeto de teste de unidade do MSTest em C# para o modelo do .NET e clique em Avançar.

    Observação

    No Visual Studio 2019 versão 16.9, o modelo de projeto MSTest é Projeto de Teste de Unidade.

  3. Nomeie o projeto como BankTests e clique em Avançar.

  4. Escolha a estrutura de destino recomendada ou o .NET 8 e escolha Criar.

    O projeto BankTests é adicionado à solução Bank.

  5. No projeto BankTests adicione uma referência ao projeto Bank.

    No Gerenciador de Soluções, selecione Dependências no projeto BankTests e, em seguida, escolha Adicionar Referência (ou Adicionar referência de projeto) no menu de clique com o botão direito do mouse.

  6. Na caixa de diálogo Gerenciador de Referências, expanda Projetos, selecione Solução e, em seguida, marque o item Banco.

  7. Selecione OK.

Criar a classe de teste

Crie uma classe de teste para verificar a classe BankAccount. Use o arquivo UnitTest1.cs que foi gerado pelo modelo do projeto, mas dê ao arquivo e à classe nomes mais descritivos.

Renomear um arquivo e uma classe

  1. Para renomear o arquivo, em Gerenciador de Soluções, selecione o arquivo UnitTest1.cs no projeto BankTests. No menu do clique com o botão direito, escolha Renomear (ou pressione F2) e, em seguida, renomeie o arquivo como BankAccountTests.cs.

  2. Para renomear a classe, posicione o cursor em UnitTest1 no editor de código, clique com o botão direito do mouse e, em seguida, escolha Renomear (ou pressione F2). Digite BankAccountTests e, em seguida, pressione Enter.

O arquivo BankAccountTests.cs agora contém o seguinte código:

// The 'using' statement for Test Tools is in GlobalUsings.cs
// using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace BankTests
{
    [TestClass]
    public class BankAccountTests
    {
        [TestMethod]
        public void TestMethod1()
        {
        }
    }
}

Adicionar uma instrução using

Adicione uma instrução using à classe de teste para permitir chamadas ao projeto em teste sem usar nomes totalmente qualificados. Na parte superior do arquivo da classe, adicione:

using BankAccountNS;

Requisitos de classe de teste

Os requisitos mínimos para uma classe de teste são:

  • O atributo [TestClass] é necessário em qualquer classe que contenha métodos de teste de unidade que você queira executar no Gerenciador de Testes.

  • Cada método de teste que você deseja reconhecer com o Gerenciador de Testes precisa ter o atributo [TestMethod].

Pode haver outras classes em um projeto de teste de unidade que não têm o atributo [TestClass] e pode haver outros métodos em classes de teste que não têm o atributo [TestMethod]. Você pode chamar essas classes e métodos dos seus métodos de teste.

Criar o primeiro método de teste

Neste procedimento, você escreverá métodos de teste de unidade para verificar o comportamento do método Debit da classe BankAccount.

Há pelo menos três comportamentos que precisam ser verificados:

  • O método lançará um ArgumentOutOfRangeException se o valor do débito for maior que o saldo.

  • O método gerará um ArgumentOutOfRangeException se o valor do débito for menor que zero.

  • Se o valor do débito for válido, o método subtrairá o valor do débito do saldo da conta.

Dica

Você pode excluir o método TestMethod1 padrão, porque você não o usará neste passo a passo.

Para criar um método de teste

O primeiro teste verifica que um valor válido (ou seja, um que seja menor que o saldo da conta e maior que zero) retira a quantidade correta da conta. Adicione o seguinte método à classe BankAccountTests:

[TestMethod]
public void Debit_WithValidAmount_UpdatesBalance()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = 4.55;
    double expected = 7.44;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act
    account.Debit(debitAmount);

    // Assert
    double actual = account.Balance;
    Assert.AreEqual(expected, actual, 0.001, "Account not debited correctly");
}

O método é simples: ele define um novo objeto BankAccount com um saldo inicial e, em seguida, retira um valor válido. Ele usa o método Assert.AreEqual para verificar se o saldo final é conforme o esperado. Métodos como Assert.AreEqual, Assert.IsTrue e outros são frequentemente usados em testes de unidade. Para obter mais informações conceituais sobre como escrever um teste de unidade, consulte Escrever seus testes.

Requisitos do método de teste

Um método de teste deve atender aos seguintes requisitos:

  • Ele está decorado com o atributo [TestMethod].

  • Ele retorna void.

  • Não pode ter parâmetros.

Criar e executar o teste

  1. No menu Build selecione Criar solução (ou pressione Ctrl + SHIFT + B).

  2. Se o Gerenciador de Testes não estiver aberto, abra-o escolhendo Teste>Gerenciador de Testes (ou Testar>Windows>Gerenciador de Testes) na barra de menus superior (ou pressione Ctrl + E, T).

  3. Escolha Executar Tudo para executar o teste (ou pressione Ctrl + R, V).

    Durante a execução do teste, a barra de status na parte superior da janela Gerenciador de Testes fica animada. Ao final da execução de teste, a barra ficará verde se todos os métodos de teste forem aprovados ou vermelha, se algum teste falhar.

    Nesse caso, o teste falha.

  4. Selecione o método no Gerenciador de Testes para exibir os detalhes na parte inferior da janela.

Corrigir o código e executar os testes novamente

O resultado do teste contém uma mensagem que descreve a falha. Talvez seja necessário fazer uma busca detalhada para ver essa mensagem. Para o método AreEqual, a mensagem exibe o que era esperado e o que foi realmente recebido. Você esperava que o saldo diminuísse, mas em vez disso, ele aumentou pelo valor do saque.

O teste de unidade revelou um bug: o valor do saque foi adicionado ao saldo da conta quando deveria ser subtraído.

Corrigir o bug

Para corrigir o erro, no arquivo BankAccount.cs, substitua a linha:

m_balance += amount;

por:

m_balance -= amount;

Executar o teste novamente

No Gerenciador de Testes, escolha Executar Todos para executar o teste novamente (ou pressione Ctrl + R, V). A barra verde/vermelho fica verde para indicar que o teste foi aprovado.

Test Explorer in Visual Studio 2019 showing passed test

Test Explorer in Visual Studio 2019 showing passed test

Usar testes de unidade para melhorar o código

Esta seção descreve como um processo iterativo de análise, desenvolvimento de testes de unidade e refatoração pode ajudá-lo a tornar seu código de produção mais robusto e eficiente.

Analisar os problemas

Você criou um método de teste para confirmar que um valor válido é deduzido corretamente no método Debit. Agora, verifique se o método gera uma ArgumentOutOfRangeException se o valor do débito é:

  • maior que o saldo ou
  • menor que zero.

Criar e executar novos métodos de teste

Crie um método de teste para verificar o comportamento correto quando o valor do débito é menor que zero:

[TestMethod]
public void Debit_WhenAmountIsLessThanZero_ShouldThrowArgumentOutOfRange()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = -100.00;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act and assert
    Assert.ThrowsException<System.ArgumentOutOfRangeException>(() => account.Debit(debitAmount));
}

Use o método ThrowsException para declarar que a exceção correta foi gerada. Esse método faz com que o teste falhe, a menos que uma ArgumentOutOfRangeException seja gerada. Se você modificar temporariamente o método em teste para gerar uma ApplicationException mais genérica quando o valor do débito for menor que zero, o teste se comportará corretamente, ou seja, ele falhará.

Para testar o caso quando o valor retirado é maior que o saldo, realize as seguintes etapas:

  1. Criar um novo método de teste chamado Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange.

  2. Copiar o corpo do método de Debit_WhenAmountIsLessThanZero_ShouldThrowArgumentOutOfRange para o novo método.

  3. Definir debitAmount para um número maior que o saldo.

Execute os dois testes e verifique se eles são aprovados.

Continuar a análise

O método que está sendo testado pode ser melhorado ainda mais. Com a implementação atual, não temos como saber qual condição (amount > m_balance ou amount < 0) levou à exceção sendo lançada durante o teste. Nós sabemos apenas que um ArgumentOutOfRangeException foi lançado em algum lugar no método. Seria melhor se pudéssemos dizer qual condição em BankAccount.Debit fez com que a exceção fosse lançada (amount > m_balance ou amount < 0) para que possamos ter certeza de que nosso método está checando corretamente seus argumentos.

Observe novamente o método em teste (BankAccount.Debit) e veja que ambas as instruções condicionais usam um construtor ArgumentOutOfRangeException que apenas usa o nome do argumento como parâmetro:

throw new ArgumentOutOfRangeException("amount");

Há um construtor que você pode usar que relata informações muito mais detalhadas: ArgumentOutOfRangeException(String, Object, String) inclui o nome do argumento, o valor do argumento e uma mensagem definida pelo usuário. Refatore o método em teste para usar esse construtor. Melhor ainda, use membros de tipo disponíveis publicamente para especificar os erros.

Refatorar o código em teste

Primeiro, defina duas constantes para as mensagens de erro no escopo da classe. Coloque as definições na classe em teste, BankAccount:

public const string DebitAmountExceedsBalanceMessage = "Debit amount exceeds balance";
public const string DebitAmountLessThanZeroMessage = "Debit amount is less than zero";

Em seguida, modifique as duas instruções condicionais no método Debit:

if (amount > m_balance)
{
    throw new System.ArgumentOutOfRangeException("amount", amount, DebitAmountExceedsBalanceMessage);
}

if (amount < 0)
{
    throw new System.ArgumentOutOfRangeException("amount", amount, DebitAmountLessThanZeroMessage);
}

Refatorar os métodos de teste

Refatore os métodos de teste removendo a chamada a Assert.ThrowsException. Encapsule a chamada a Debit() em um bloco try/catch, capture a exceção específica que é esperada e verifique a mensagem associada. O método Microsoft.VisualStudio.TestTools.UnitTesting.StringAssert.Contains fornece a capacidade de comparar duas cadeias de caracteres.

Agora, o Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange pode ser parecido com este:

[TestMethod]
public void Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = 20.0;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act
    try
    {
        account.Debit(debitAmount);
    }
    catch (System.ArgumentOutOfRangeException e)
    {
        // Assert
        StringAssert.Contains(e.Message, BankAccount.DebitAmountExceedsBalanceMessage);
    }
}

Testar, gravar e analisar novamente

Atualmente, o método de teste não manipula todos os casos que deveria. Se o método em teste, o método Debit, não lançar um ArgumentOutOfRangeException quando o debitAmount for maior que o saldo (ou menor que zero), o método de teste será aprovado. Esse cenário não é bom porque você deseja que o método de teste falhe se nenhuma exceção for lançada.

Esse resultado é um bug no método de teste. Para resolver o problema, adicione uma declaração Assert.Fail ao final do método de teste para lidar com o caso em que nenhuma exceção é gerada.

Uma nova execução do teste mostra que agora o teste falha se a exceção correta é capturada. O bloco catch captura a exceção, mas o método continua sendo executado e ele falha na nova declaração Assert.Fail. Para resolver esse problema, adicione uma instrução return após a StringAssert no bloco catch. Uma nova execução do teste confirma que você corrigiu o problema. A versão final de Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange é parecida com esta:

[TestMethod]
public void Debit_WhenAmountIsMoreThanBalance_ShouldThrowArgumentOutOfRange()
{
    // Arrange
    double beginningBalance = 11.99;
    double debitAmount = 20.0;
    BankAccount account = new BankAccount("Mr. Bryan Walton", beginningBalance);

    // Act
    try
    {
        account.Debit(debitAmount);
    }
    catch (System.ArgumentOutOfRangeException e)
    {
        // Assert
        StringAssert.Contains(e.Message, BankAccount.DebitAmountExceedsBalanceMessage);
        return;
    }

    Assert.Fail("The expected exception was not thrown.");
}

Conclusão

As melhorias no código de teste levaram a métodos de teste mais robustos e informativos. Porém, o mais importante é que eles também melhoraram o código em teste.

Dica

Este passo a passo usa a estrutura de teste de unidade do Microsoft para código gerenciado. O Gerenciador de Testes também pode executar testes em estruturas de teste de unidade de terceiros que têm adaptadores para o Gerenciador de Testes. Para saber mais, consulte Instalar estruturas de teste de unidade de terceiros.

Para obter informações sobre como executar testes em uma linha de comando, confira Opções de linha de comando de VSTest.Console.exe.