O NUnit é um dos frameworks para realização de testes unitários em .NET mais utilizados no mercado. Ele conta com diversas classes e métodos que facilitam a escrita e execução dos testes. Nas seções a seguir você verá como instalá-lo em um projeto e como executar os testes a partir do Test Explorer no Visual Studio.

Visão geral

Teste unitário é uma técnica que consiste de colocar à prova as diversas funcionalidades de um sistema, testando-as em suas menores unidades. Ou seja, de forma geral os testes unitários se concentram em validar o funcionamento das diversas funções de uma aplicação. A motivação dessa técnica é não apenas garantir que os métodos funcionem como esperado, mas também que eles continuem funcionando ao longo de todo o ciclo de vida do projeto, mesmo após modificações. Dessa forma, os testes são executados diversas vezes durante o desenvolvimento e por isso é fundamental termos à nossa disposição ferramentas que nos auxiliem nessa tarefa.

Instalação

Para demonstrar a utilização do NUnit tomaremos como exemplo um projeto do tipo Class Library, que contém classes auxiliares contendo funções que precisam ser testadas. No momento, temos apenas a classe ValidadorCPF com o método Validar. Na Figura 1 podemos ver a estrutura da Solution no Visual Studio:

image alt text

Figura 1. Solution com um projeto Class Library contendo uma classe que precisa ser testada

Por se tratar de um projeto Class Library, ele não pode ser executado diretamente. Logo, para testar a classe ValidadorCPF seria necessário criar um outro projeto executável, como uma Console Application. No entanto, existe no Visual Studio um tipo de projeto específico para a realização de testes. Para adicioná-lo à solution, devemos clicar com a direita sobre ela e em Add > New project. Em seguida, devemos selecionar a categoria Tests e o template Unit Test Project, como mostra a Figura 2.

image alt text

Figura 2. Criando um novo projeto de testes unitários

Convencionou-se criar um projeto de testes para cada projeto a ser testado na solution. Neste caso, como temos uma Class Library chamada Sistema.Helpers, criamos um Unit Test Project chamado Sistema.Helpers.Tests. A solution então ficará como mostra a Figura 3.

image alt text

Figura 3. Solution contendo um novo projeto de testes

Com o projeto criado, o próximo passo é abrir o gerenciador de pacotes do Nuget, clicando com a direita sobre o projeto Sistema.Helpers.Tests e em Manage Nuget Packages.

Na aba Installed devemos desinstalar os pacotes MSTest.TestAdapter e MSTest.TestFramework. Eles representam o framework de testes padrão da Microsoft, o MSTest, que vem instalado nesse tipo de projeto, mas que aqui será substituído pelo NUnit.

Em seguida, na aba Browse devemos buscar por NUnit e instalar os pacotes NUnit e NUnit3TestAdapter. O primeiro representa o próprio framework de testes e o segundo é uma ferramenta adicional para que seja possível executar os testes a partir da janela Test Explorer do Visual Studio (que por padrão funciona com o MSTest).

Configuração

Com o framework instalado, podemos então abrir o arquivo UnitTest1.cs, que é criado por padrão e neste momento estará com alguns erros, devido à remoção do MSTest. Neste arquivo, faremos as seguintes modificações:

  • Remover a referência ao namespace Microsoft.VisualStudio.TestTools.UnitTesting;
  • Adicionar a referência ao namespace NUnit.Framework;
  • Remover a anotação [TestClass] da classe UnitTest1 e adicionar a anotação [TestFixture];
  • Remover a anotação [TestMethod] do método TestMethod1 e adicionar a anotação [Test].

Escrevendo Testes

A anotação TestFixture marca uma classe como “classe de testes” e permite que ela seja reconhecida como tal no Test Explorer. Já a anotação Test define um método de teste. Para cada teste a ser realizado precisaremos ter um método anotado com esse atributo. Outro ponto importante que deve ser observado é a nomenclatura das classes e métodos. Devemos sempre usar nomes claros que permitam saber o que aquela classe é responsável por testar e o que cada método pretende avaliar.

Visão geral

Neste caso, vamos modificar o nome da classe UnitTest1 para TestesValidadorCPF (ou ValidadorCPFTests), indicando que ela é responsável por testar a classe ValidadorCPF. Em seguida, vamos renomear o método TestMethod1 para DeveRetornarFalsoQuandoCPFVazio. Note que o nome deixa claro o caso de teste (CPF vazio) e o resultado esperado (falso).

Agora podemos escrever o teste de fato. Para isso é necessário adicionar uma referência ao projeto Sistema.Helpers, clicando com a direita sobre o projeto de testes e em Add > Reference. Na seção Projects devemos então selecionar Sistema.Helpers e clicar em OK.

Em seguida podemos adicionar na classe ValidadorCPFTestes uma instrução using referenciando o namespace Sistema.Helpers. Por fim o método de testes ficará da seguinte forma:

[Test]
public void DeveRetornarFalsoQuandoCPFVazio()
{
    var resultado = ValidadorCPF.Validar("");
    Assert.AreEqual(false, resultado);
}

Na primeira linha invocamos o método a ser testado, passando como argumento uma string vazia, que representa um CPF inválido. Em seguida usamos a classe Assert para nos certificar de que o resultado da validação é falso.

A classe Assert possui diversos métodos que devem ser usados para avaliar os resultados obtidos, comparando-os com valores esperados.

Na prática

Exemplo 1

Saber escrever testes eficientes é tão importante quanto conhecer um framework como o NUnit. Quando queremos validar o funcionamento de um método é preciso saber a quais cenários ele deve ser submetido e qual será o resultado esperado em cada um. Por exemplo, o método de validação de CPF deve retornar falso quando o parâmetro for vazio, quando contiver letras ou caracteres diferentes de ponto e traço, quando o comprimento for maior ou menor que o esperado, etc. Cada situação dessas deve ser avaliada em um método de teste diferente.

Há, no entanto, cenários que para serem validados precisam ser testados com argumentos semelhantes. Por exemplo, CPFs contendo apenas um mesmo dígito, ou ainda vários CPFs válidos. Para essas situações não é necessário escrever inúmeros métodos de testes. Podemos usar o atributo TestCase e formar vários casos de teste para um mesmo método. Um exemplo disso pode ser visto abaixo:

[TestCase("00000000000", ExpectedResult = false, TestName = "Todos os dígitos zero")]
[TestCase("11111111111", ExpectedResult = false, TestName = "Todos os dígitos um")]
[TestCase("22222222222", ExpectedResult = false, TestName = "Todos os dígitos dois")]
public bool DeveRetornarFalsoQuandoTodosOsDigitosSaoIgais(string cpf)
{
    return ValidadorCPF.Validar(cpf);
}

O teste em questão é “Deve retornar falso quando todos os dígitos são iguais” e recebe vários argumentos diferentes. O primeiro parâmetro do atributo TestCase será repassado para o método de teste (argumento cpf). Já o parâmetro ExpectedResult indica qual é o resultado esperado desse caso de teste e o TestName dá um nome a ele.

Observe também que aqui não foi preciso usar a classe Assert. Ao invés disso o retorno do método deve ser o resultado a ser comparado com o ExpectedResult.

Exemplo 2

Em algumas situações precisamos executar um teste com um conjunto de dados, a fim de garantir que a unidade testada funciona corretamente para todas as entradas fornecidas. Ao invés de escrever um método de teste para cada entrada, podemos usar o atributo TestCaseSource e definir em uma classe separada esse conjunto de dados de entrada.

[TestFixture]
public class TestesValidadorEmail
{
   [Test, TestCaseSource(typeof(CasosDeTesteDeEmail), "EmailsInvalidos")]
   public bool ValidarEmailsInvalidos(string email) => ValidadorEmail.Validar(email);
}

public class CasosDeTesteDeEmail
{
   public static List EmailsInvalidos
   {
       get
       {
           return new List()
           {
               new TestCaseData("").Returns(false),
               new TestCaseData("email").Returns(false),
               new TestCaseData("email@email").Returns(false),
               new TestCaseData("email.com").Returns(false)
           };
       }
   }
}

Neste exemplo o método de teste ValidarEmailsInvalidos espera como parâmetro o email a ser validado e retorna um booleano indicando o sucesso ou falha da validação, que é feita pela classe ValidadorEmail. Além do atributo Test, esse método recebeu também o atributo TestCaseSource, indicando que os dados para teste vêm da classe CasosDeTesteDeEmail, na sua propriedade EmailsInvalidos. Logo abaixo vemos como devem ser definidos os casos de teste: a classe deve retornar uma lista de objetos do tipo TestCaseData, que são instanciados da seguinte forma: new TestCaseData(parametros).Returns(resultadoEsperado). Com isso o método ValidarEmailsInvalidos será executado quatro vezes e seu resultado será comparado com o que foi especificado nos casos de teste.

Exemplo 3

Testar se um método está lançando uma exceção em determinados cenários também é uma situação comum. Por exemplo, quando os parâmetros recebidos são considerados inválidos, o método pode lançar exceções do tipo ArgumentException. Para testar esse tipo de cenário podemos usar o método Assert.Throws, da seguinte forma:

[TestFixture]
public class TestesAgendaService
{
   [Test, TestCaseSource(typeof(CasosDeTesteDeEventos), "DatasInvalidas")]
   public void TestarInsercaoComDatasInvalidas(DateTime inicio, DateTime fim, string descricao)
   {
       Assert.Throws(
           typeof(ArgumentException),
           () => { new AgendaService().CriarEvento(inicio, fim, descricao); }
       );
   }
}

public class CasosDeTesteDeEventos
{
   public static List DatasInvalidas
   {
       get
       {
           return new List
           {
               new TestCaseData(DateTime.Today.AddDays(-1), DateTime.Today.AddDays(1), "Data inicial passada"),
               new TestCaseData(DateTime.Today.AddDays(1), DateTime.Today.AddDays(-1), "Data final passada"),
               new TestCaseData(DateTime.Today.AddDays(-1), DateTime.Today.AddDays(-1), "Ambas datas passadas"),
               new TestCaseData(DateTime.Today.AddDays(-1), DateTime.Today.AddDays(-1), "Data final menor que inicial")
           };
       }
   }
}

O método TestarInsercaoComDatasInvalidas tem por objetivo validar se o método CriarEvento da classe AgendaService está lançando uma exceção do tipo ArgumentException quando as datas informadas forem inválidas (datas anteriores ao dia atual ou data final anterior à inicial). O primeiro parâmetro do método Assert.Throws é o tipo da exceção que deve ser lançada, por isso usamos o operador typeof. Já o segundo argumento é um delegate que executa o procedimento a ser testado (e que deve lançar a exceção).

Exemplo 4

O NUnit também oferece um atributo que pode ser usado para executar um método de teste várias vezes seguidas. Ele é útil, por exemplo, quando queremos validar que dois registros iguais não sejam inseridos no banco de dados. Ou seja, podemos executar o método de inserção pelo menos duas vezes e a segunda delas deve falhar.

[Test, Repeat(2)]
public void TestarInsercoesSeguidas()
{
   new AgendaService().CriarEvento(DateTime.Now,
                                   DateTime.Now.AddDays(1),
                                   "Novo evento");
}

O atributo Repeat define que o método TestarInsercoesSeguidas deve ser executado duas vezes. Se em uma delas o teste falhar, então o resultado geral é apresentado como falha. Por isso, se quisermos avaliar o lançamento de uma exceção nesse caso, podemos usar o método Assert.Throws, visto anteriormente.

Exemplo 5

Testar métodos que retornam uma lista de objetos normalmente requer avaliar todos os itens dessa lista. Por exemplo, podemos avaliar se um determinado método que lista os eventos de datas anteriores retorna todos os objetos com o status “Passado” (eventos com data passada).

[Test]
public void TestarEventosPassadosPorData()
{
   Assert.That(
       new AgendaService().ListarEventosPorData(DateTime.Today.AddDays(30), DateTime.Today.AddDays(-1)),
       Has.All.Property("Status").EqualTo(StatusEvento.Passado)
   );
}

O método ListarEventosPorData, da classe AgendaService, deve retornar uma lista de eventos cujas datas inicial e final estejam no intervalo passado como parâmetro. Nesse caso, como informamos datas passadas, todos os eventos retornados devem estar com o status Passado. Para isso construímos a expressão Has.All.Property(“Status”).EqualsTo(StatusEvento.Passado). O método All permite avaliar todos os itens da coleção retornada, enquanto o método Property avalia a existência de uma propriedade com esse nome e o EqualsTo verifica se essa propriedade tem o valor igual ao que foi informado.

Executando os Testes

Frameworks de teste unitário comumente possuem uma forma específica para testar seu código sem executar o sistema manualmente. O NUnit conta com o poder do Visual Studio para executar os testes unitários e exibir se os testes obtiveram sucesso ou falharam.

Podemos agora abrir a janela Test Explorer do Visual Studio e nela estarão listados todas as classes e métodos de testes existentes no projeto (apenas um até agora), como mostra a Figura 4.

image alt text

Figura 4. Janela Test Explorer do Visual Studio

No topo dessa janela podemos clicar em Run All para executar todos os testes. Logo em seguida os resultados serão exibidos nessa mesma janela, como ilustra a Figura 5.

image alt text

Figura 5. Resultado da execução dos testes

Com isso em mente, agora no Test Explorer cada caso será listado individualmente, como vemos na Figura 6.

image alt text

Figura 6. Casos de teste listados no Test Explorer

Clicando novamente em Run All temos o resultado (Figura 7):

image alt text

Figura 7. Novo resultado dos testes

Neste caso todos os testes falharam, o que indica um erro no nosso método de validação. Clicando sobre cada teste podemos ver logo abaixo o resultado esperado e o obtido, o que nos ajuda a identificar a falha. Podemos então fazer as correções necessárias e executar novamente os testes.

A partir do momento em que um método passou nos testes, ela deve continuar assim durante todo o desenvolvimento da aplicação. Ou seja, quando escrevemos testes unitários não devemos nos concentrar apenas naquele momento, mas sim garantir de que os testes aprovados continuem assim, pois o contrário indicaria que problemas foram inseridos no código.

Assertions

As assertions são verificações cujo objetivo é validar os resultados dos métodos que estão sendo testados. Ou seja, elas validam se o resultado foi o esperado e o teste passou, ou se ele falhou.

No NUnit essas verificações são feitas a partir da classe Assert e do método That, que recebe como parâmetro a condição a ser avaliada. Em sua forma mais simples o método Assert.That recebe um argumento booleano, que se for verdadeiro indica que o teste passou e caso contrário que o teste falhou:

[Test]
public void DeveSerFalsoQuandoCpfForTodoZero()
{
    Assert.That(ValidadorCPF.Validar("00000000000"));
}

Há, ainda, outras sobrecargas do método That que nos permitem construir essa verificação de forma mais elaborada, fazendo diversas validações sobre o resultado do método a ser testado. Essas regras são construídas a partir de classes e métodos que quando usados em conjunto formam expressões semanticamente claras para quem está lendo o código. Por exemplo:

Assert.That(numero, Is.EqualTo(2));

No exemplo acima verificamos se a variável numero “é igual” a 2. Note que a expressão Is.EqualTo pode ser lida de forma quase tão natural quanto uma frase escrita em inglês. Da mesma forma, se quisermos fazer a verificação contrária podemos usar o operador Not:

Assert.That(2, Is.Not.EqualTo(2));

Abaixo vemos uma lista das constantes que podem ser usados a partir da classe Is no formato “Is.Condicao”:

Constate Exemplo de uso Descrição
True / False Assert.That(valor, Is.True)
Assert.That(valor, Is.False)
Valida se o primeiro parâmetro é um booleano verdadeiro ou falso.
Positive / Negative Assert.That(numero, Is.Positive)
Assert.That(numero, Is.Negative)
Verifica se o valor numérico é positivo ou negativo
Null Assert.That(valor, Is.Null) Valida se o valor é nulo
Zero Assert.That(valor, Is.Zero) Testa se o número é zero

Há ainda constantes que podem ser usadas para validar coleções de objetos:

Constante Exemplo de Uso Descrição
Empty Assert.That(colecao, Is.Empty) Valida se uma coleção é vazia
Unique Assert.That(colecao, Is.Unique) Valida se todos os itens de uma coleção são únicos (não se repetem)
Ordered Assert.That(colecao, Is.Ordered) Valida se uma coleção está ordenada

Além dessas constantes existem vários métodos que também podem ser usados para avaliar uma condição:

Método Exemplo de Uso Descrição
EqualTo Assert.That(valor, Is.EqualTo(outro)) Valida se um valor é igual a outro
AtLeast / AtMost Assert.That(valor, Is.AtLeast(minimo))
Assert.That(valor, Is.AtMost(maximo))
Testa se um valor é pelo menos (maior ou igual) ou no máximo (menor ou igual) outro valor
GreaterThan / LessThan Assert.That(valor, Is.GreaterThan(outro))
Assert.That(valor, Is.LessThan(outro))
Verifica se um valor é maior que ou menor que outro
GreaterThanOrEqualTo / LessThanOrEqualTo Assert.That(valor, Is.GreaterThanOrEqualTo(outro))
Assert.That(valor, Is.LessThanOrEqualTo(outro))
Verifica se um valor é maior ou igual ou menor ou igual a outro
TypeOf Assert.That(variavel, Is.TypeOf(tipo)) Testa se a variável é de um determinado tipo
AnyOf Assert.That(valor, Is.AnyOf(1, 2, 3)) Valida se um valor é igual a pelo menos um dos que é passado por parâmetro
InRange Assert.That(valor, Is.InRange(inicio, fim)) Verifica se o valor está no intervalo passado como parâmetro, incluindo os extremos

E assim como as constantes, há métodos dedicados à validação de coleções:

Método Exemplo de Uso Descrição
EquivalentTo Assert.That(lista, Is.EquivalentTo(outraLista)) Testa se uma coleção contém os mesmos itens de outra, não necessariamente na mesma ordem
SubsetOf / SupersetOf Assert.That(lista, Is.SubsetOf(outraLista))
Assert.That(lista, Is.SupersetOf(outraLista))
Verifica se uma coleção é um subconjunto (está contida) ou superconjunto (contém) de outra

Por fim, temos ainda os operadores de junção, usados para comparar duas condições em conjunto:

Operador Exemplo de Uso Descrição
All Assert.That(lista, Is.All.Positive) Testa se todos os itens de uma coleção atendem a uma condição, especificada por meio de outras constantes e métodos vistos anteriormente
And Assert.That(valor, Is.Positive.And.LessThan(10)) Une duas condições pelo operador lógico E (and), retornando true se as duas forem verdeiras
Or Assert.That(valor, Is.Zero.Or.Null) Une duas condições pelo operador lógico OU (or), retornando true se pelo menos uma delas for verdadeira
Not Assert.That(valor, Is.Not.Null) Nega uma condição

Utilizando essas constantes, operadores e métodos, além de outros que estão disponíveis no framework, podemos construir testes robustos, capazes de fazer as devidas validações por meio de um código bastante claro...