C#

17 out, 2022

Primeiros testes unitários em C# com xUnit (parte 1)

Publicidade

Se você começou na programação há pouco tempo e está procurando aprender sobre testes unitários, saiba que está em um ótimo caminho! Ao se aprofundar em testes unitários, por consequência, você estará em contato com princípios como SOLID, por exemplo. Só nessas letrinhas há muito aprendizado que lhe recomendo fortemente a buscar aprender mais sobre, mas neste artigo focaremos no seu primeiro teste. E não há como falar sobre testes unitários sem citar a famosa pirâmide de testes.

Existem diversos tipos de teste, mas na pirâmide estão os mais populares. O que você precisa entender é que em direção ao topo estão os testes mais demorados de se fazer e mais caros também. Se tratando dos testes unitários, eles são os mais rápidos e baratos de se fazer. Agora eu preciso de te contar alguns fatos sobre testes unitários que julgo importante você saber:

     Testes aumentam a qualidade do software, mas não o torna à prova de falhas.

     Alguns testes geram mais código do que o próprio código que está sendo testado.

     Se o software não foi orientado a testes durante seu desenvolvimento, certamente parte dele será refatorado.

     Não precisamos testar 100% do sistema. Isso não faz sentido em termos de tempo/custo e nem todo o sistema possui regras em todos os lugares. Testamos pontos sensíveis ou onde há regras de negócio.

     Nem tudo pode ser testado de forma unitária. Testes unitários em hipótese alguma acessam banco de dados ou serviços externos como uma API por exemplo.

     Testes unitários precisam ser rápidos. E quando digo rápido, estou falando de algo muito inferior a um segundo. Caso contrário seu teste precisa ser revisto.

Conhecendo os projetos

O projeto que usarei neste artigo foi construído usando dotnet 6 e você o encontra neste link. O framework de testes que utilizaremos é o xUnit. Existem outros como o MsTest feito pela própria Microsoft e o nUnit que inicialmente era um porte do jUnit (framework de teste para Java) e posteriormente seria totalmente reescrito. Bom, talvez agora você se pergunte por que estamos utilizando o xUnit. Eu o utilizo por ser mais simples e manter uma instância única por método de teste. Você pode encontrar mais detalhes neste artigo.

Há dois projetos em nossa solução. O primeiro é um class library que contém somente uma classe sendo uma representação de uma calculadora. A classe possui um método que realiza a soma de dois valores. E este método soma somente números maiores ou iguais a zero. Se um dos parâmetros for negativo, o método retorna -1 por padrão.

public static int Sum(int firstNumber, int secondNumber)
{
    if (firstNumber < 0 || secondNumber < 0)
    {
        return -1;
    }

    return firstNumber + secondNumber;
}

O outro projeto é onde está o nosso teste. Para criar o projeto de teste:

     Se estiver utilizando o Visual Studio, procure por xUnit Test Project.

     Se estiver utilizando a linha de comando, utilize dotnet new xunit -o <Nome do Projeto>.

Agora vamos conhecer nossa classe de teste. A única diferença dela para uma classe comum é que você precisa usar a notação fact para que o método seja reconhecido como um método de teste.

Organizando o teste

O código de teste precisa estar organizado, assim como o código do sistema. Se alguma regra do sistema mudar, o teste precisará de manutenção. E uma boa prática para isso é definir três áreas dentro de cada método de teste:

     Arrange: nesta seção você prepara todo o setup do seu teste criando as variáveis que serão utilizadas.

     Act: aqui você invoca o método que será testado.

     Assert: aqui você coloca as verificações que serão necessárias para garantir que seu teste deu certo ou não.

Essas “seções” são meramente comentários para fins de organização. Exemplo:

[Fact]
public void Test1()
{
    //arrange

    //act

    //assert
}

A princípio você pode não ver tanto valor, mas te garanto que isso fará diferença em casos mais complexos.

E agora vamos falar sobre o nome do seu método de teste. Eu costumo utilizar uma técnica chamada Given-When-Then. A ideia é que tenhamos um template que terá estas três palavras definidas. Esta abordagem faz parte do BDD, mas o conceito se aplica a qualquer coisa.

Nomenclaturas

E agora vamos falar sobre o nome do seu método de teste. Eu costumo utilizar uma técnica chamada Given-When-Then. A ideia é que tenhamos um template que terá estas três palavras definidas. Esta abordagem faz parte do BDD, mas o conceito se aplica a qualquer coisa.

     Given: define o cenário/situação que deve acontecer para que o requisito aconteça.

     When: este é o gatilho da situação.

     Then: resultado esperado após o ter acontecido o gatilho.

Considerando que iremos testar o método Sum, o nosso método ficaria assim:

GivenValidNumbers_WhenSum_ThenShouldSucccess

Obs: somente nos métodos de teste eu costumo usar underlines para separar cada grupo: Given-When-Then.

Ainda sobre nomenclaturas, podemos deixar o nosso descritível como a linguagem humana. Na notação Fact podemos utilizar um atributo chamado DisplayName e informar uma string que será exibida no lugar do método no test explorer (no Visual Studio/VS Code) ou na linha de comando. Nosso método ficaria assim:

[Fact(DisplayName = "Given valid numbers, when sum then should succcess.")]

E na linha de comando, por exemplo, ficaria assim. Muito mais amigável, não?

Agora que nosso caso de teste está muito bem descrito, vamos implementar nosso primeiro teste! Ele ficará dessa forma:

[Fact(DisplayName = "Given valid numbers, when sum then should succcess.")]
public void GivenValidNumbers_WhenSum_ThenShouldSucccess()
{
    //arrange
    const int resultExpected = 5;
    const int firstNumber = 3;
    const int secondNumber = 2;

    //act
    var resultActual = Calculator.Sum(firstNumber, secondNumber);

    //assert
    Assert.Equal(resultExpected, resultActual);
}

Na seção arrange preparamos as variáveis que serão utilizadas como parâmetro no método Sum. E a variável resultExpected tem pré-definido o valor que esperamos que o método retorne e utilizaremos para comparar ao final. Você não é obrigado a usar constantes ou invés de variáveis, mas como o valor não será alterado ao longo do nosso teste, prefiro utilizá-las.

Na seção act invocamos o método que será testado e armazeno numa variável o seu retorno para verificá-lo na seção seguinte.

E por fim, na seção assert, utilizaremos a classe de mesmo nome fornecida pelo xUnit para verificar se o valor que o método testado retornou confere com o que esperamos.

Observação 1: ao invés de utilizarmos variáveis para guardar os valores que serão posteriormente serão utilizadas em no método que será testado, por que não informamos os valores diretamente no método? Afinal de contas, serão duas variáveis a menos, não? Sim, serão duas variáveis a menos, mas a ideia aqui é dar contexto ao teste. Diga não aos “números mágicos”.

Observação 2: costumo nomear com o sufixo Expected a variável que terá o valor que espero no teste, e com o sufixo Actual a variável que tem o valor da execução do nosso método. E de onde eu tirei isso? Se você olhar o reparar nos parâmetros do método Assert.Equal, eles possuem justamente estes nomes.

Agora nós executamos nosso teste, verificamos que passou com sucesso e acabou. Certo? Na verdade, não! Existe uma métrica chamada de cobertura de testes e vamos aprender sobre ela na continuação deste artigo.