DEV Community

👨‍💻 Lucas Silva
👨‍💻 Lucas Silva

Posted on

Testes de unidade com NUnit para aplicações C# .Net -- Parte 1

Basicamente, testes automatizados são uma forma de automatizar o processo de revisão e validação de um software através de uma ferramenta programável. Esse processo, que antes podia ser extremamente trabalhoso e demorado, pode ser feito em minutos -- ou até mesmo segundos, dependendo dos tipos e da quantidade de testes existentes na sua aplicação.

Mike Cohn criou o conceito de pirâmide de testes no livro "Succeeding with Agile". É basicamente uma forma de te fazer pensar nas várias camadas de testes e em como elas devem estar presentes:
Pirâmide de testes cunhada po Cohn. A base dela é composta por testes de unidade, seguida por testes de serviços e por fim testes de ui. Quanto mais baixo o nível do teste na pirâmide, mais desacoplado e mais rápido ele é
Quanto mais baixo o nível do teste na pirâmide, mais desacoplado e mais rápido ele é, além de mais numeroso.

Há inúmeros tipos de teste -- muitos dos quais eu mesmo desconheço a fundo -- mas abordarei apenas os testes de unidade. Eles possuem esse nome pois tendem as menores unidades de classes e métodos de forma isolada do resto da aplicação: sem acoplamento, nem nada. Apenas verificações para garantir que eles funcionem conforme o esperado, que as variáveis tenham os valores esperados etc.

Uma disciplina importante para criar testes é aquela chamada de TDD - Desenvolvimento orientado a testes. Nela, primeiro se escreve os testes, depois o código para que o teste funcione, depois o código é refatorado para que se torne a melhor versão que puder ser (esse último passo costuma ser bastante ignorado, gerando débitos técnicos).

Ok, mas por que começar pelos testes e não pelo código ? Porque para criar uma solução, é necessário saber o que precisa ser criado. É necessário saber de antemão o que uma classe deve fazer, o que um método deve fazer, o valor que uma variável deve ter etc. Sem isso em mente, é muito fácil assumir que um resultado qualquer é o correto. E é assim que nascem os bugs 🐞!

Sem mais delongas, bora pro código.

Killjoy do jogo Valorant, em frente a um computador, arrumando os óculos no próprio rosto

Mãos à obra

Criei uma aplicação console para exemplificar. Nela, criarei uma classe "Conta" com as seguintes especificações:

  • Uma pessoa pode depositar qualquer valor maior que zero na conta;
  • Uma pessoa pode sacar valores maiores que 0 e menores ou iguais ao saldo total;

Adicionei um projeto NUnit com o Visual Studio. Nele, criei uma classe chamada ContaTests, conforme abaixo. O atributo [TestFixture] indica que uma classe possui testes.

[TestFixture]
public class ContaTests
{
}
Enter fullscreen mode Exit fullscreen mode

E então parti para o teste do primeiro método. Para que um método seja detectado como teste, é necessário adicionar o atributo [Test] acima dele. Quanto ao nome do método de teste, fiz baseando-me em algumas recomendações que podem ser encontradas neste link..

[Test]
public void Depositar_RetornaSaldoMaisValorDepositado()
{
    // Arrange 

    // Act

    // Assert
}
Enter fullscreen mode Exit fullscreen mode

Gosto de deixar os 3 comentários em todos os testes que crio para separar bem as fases dele. Essas fases formam o padrão AAA, muito utilizado na indústria, e correspondem a:

  • Arrange: fase na qual é necessário preparar teste preparando as variáveis, criando instâncias de objeto necessárias etc.;
  • Act: fase na qual o método a ser testado é executado;
  • Assert: fase na qual é feita a verificação de que o resultado obtido está de acordo com o esperado.

Na primeira fase, criei apenas uma instância da classe Conta, que ainda não existe, e então criei uma variável com o valor que espero obter como resultado do teste.

// Arrange 
Conta conta = new Conta("Lucas", 100);
decimal saldoEsperado = 200;
Enter fullscreen mode Exit fullscreen mode

Na fase de ação, executei o método e armazenei o resultado em uma variável:

// Act
var resultado = conta.Depositar(100);
Enter fullscreen mode Exit fullscreen mode

E por fim, fiz a verificação de que o resultado era igual ao saldoEsperado. Para isso, usei a classe Assert do NUnit. Ela contém vários métodos estáticos para testar inúmeras situações, como listas, igualdades, nulos etc.

// Assert
Assert.AreEqual(saldoEsperado, resultado);
Enter fullscreen mode Exit fullscreen mode

O teste final ficou da seguinte forma:

[Test]
public void Depositar_RetornaSaldoMaisValorDepositado()
{
    // Arrange 
    Conta conta = new Conta("Lucas", 100);
    decimal saldoEsperado = 200;

    Act
    var resultado = conta.Depositar(100);

    Assert
    Assert.AreEqual(saldoEsperado, resultado);
}
Enter fullscreen mode Exit fullscreen mode

O teste não compilará no primeiro momento pois a classe Conta ainda não existe. Logo, decidi criá-la:

public class Conta
{
    public Conta(string cliente, decimal saldo)
    {
        Cliente = cliente;
        Saldo = saldo;
    }

    public string Cliente { get; private set; }
    public decimal Saldo { get; private set; }

    public decimal Depositar(decimal valor)
    {
        Saldo += valor;
        return Saldo;
    }
}
Enter fullscreen mode Exit fullscreen mode

Aproveitei e também criei o método Depositar. Ele é simples: recebe um valor e então adiciona esse valor à propriedade Saldo da classe.

Adicionei uma referência da classe no projeto de teste e então o executei. E então o teste passou !
A imagem mostra que o teste foi aprovado após a execução com os valores corretos

⚠️⚠️ Uma forma de garantir que o teste funciona é fazendo-o quebrar propositalmente. Uma forma de quebrar o teste acima seria alterando o valor esperado para 300. Se o teste e o código estiverem certos, o teste não passará. Isso é importante para evitar que o resultado seja um falso-positivo.⚠️⚠️

Ok, o valor adicionado está correto, a soma está correta. Mas não é tudo a ser testado nesse método:

  • Uma pessoa pode depositar qualquer valor maior que zero na conta;

Logo, é necessário criar um teste para verificar se o meu programa barra o depósito de um valor negativo como -R$100.

[Test]
public void Deposito_QuandoExecutadoComValorNegativo_LancaLancaInvalidOperationException()
{
    //Arrange
    Conta conta = new Conta("Lucas", 100);

    //Act
    Func<decimal> acao = () => conta.Depositar(-100);

    //Assert
    Assert.Throws<InvalidOperationException>(() => acao());
}
Enter fullscreen mode Exit fullscreen mode

A grande diferença está nas fases Act e Assert. Assim que o valor -100 for detectado pelo método, é necessário que ele lance uma nova exceção: InvalidOperationException. Quando ela for lançada, o programa sairá daquele método antes de completá-lo, fazerndo com que o assert seja um pouco diferente. Para testar se ela realmente foi lançada, primeiro armazenei o método em um delegate (eu poderia explicar o significado mas tanto a Microsoft quanto o Macoratti fizeram um excelente trabalho falando sobre) e então utilizei o método Throws da classe Assert. Ele recebe o tipo da exceção como generic (artigo da Microsoft e artigo do Macoratti) e então consegue verificar se a exceção foi lançada.

Ao rodar o teste na primeira vez, ele não passoupois o método ainda não lança a exceção. Adicionei apenas 1 if para lançar a exceção e então o teste passou !

public decimal Depositar(decimal valor)
{
    if (valor <= 0)
    {
        throw new InvalidOperationException("Não é possível depositar valores negativos");
    }
    Saldo += valor;
    return Saldo
}
Enter fullscreen mode Exit fullscreen mode

⚠️⚠️Para quebrar o teste e verificar se ele não dá um falso positivo, é possível simplesmente alterar o tipo da exceção esperada no Assert.⚠️⚠️

Por se tratar de um exemplo, o método de saque terá testes bastante parecidos com o do depósito, então é possível verificá-lo, assim como todo o resto do código, nesse repositório do GitHub que criei!

Bem, essa foi uma breve introdução sobre testes de unidade. Na próxima parte falarei sobre Mocks em um exemplo de uma API Rest e sobre o método SetUp.Se curtiu, não esqueça de compartilhar a postagem!

Até a próxima !

Snoopy Dogg acenando em despedida

Discussion (0)