ASP.NET Core Web API - Testes Unitários com xUnit
  Neste artigo veremos como criar casos de testes para uma API Asp.Net Core usando xUnit.


O xUnit é uma ferramenta de teste de unidade gratuita, de código aberto e focada na comunidade para aplicativos .NET. Por padrão, o .Net também fornece um modelo de projeto xUnit para implementar casos de teste.
 


 

Os casos de teste de unidade baseiam-se na fórmula 'AAA' que significa 'Arrange', 'Act' e 'Assert' que numa tradução livre seria 'Organizar' , 'Agir' e 'Afirmar', onde :

XUnit - Conceitos básicos

No xUnit precisamos decorar os métodos de teste com o atributo [Fact], que é usado pelo xUnit para marcar os métodos de testes. Além dos métodos de testes, também podemos ter vários métodos auxiliares na classe de teste.

Com o XUnit para tornar um método comum em método de testes basta adicionar [Fact] ou [Theory] acima de sua assinatura, os atributos diferem apenas no seguinte, para testes sem parâmetros deve-se usar [Fact], para testes como parâmetros utiliza-se o [Theory].

O atributo [Theory] indica um teste parametrizado que é verdadeiro para um subconjunto de dados. Esses dados podem ser fornecidos de várias maneiras, mas o mais comum é com um atributo [InlineData]. Assim este atributo permite executar um método de teste várias vezes passando diferentes valores a cada vez como parâmetros.

Podemos ainda desativar um teste por qualquer motivo. Para isso basta definir a propriedade Skip no atributo Fact com o motivo que você desativou o teste (o motivo não é exibido).

        [Fact(Skip = "Teste ainda não disponível")]
        public void Teste()
        {
        }

Á medida que o número de seus testes aumenta, você pode organizá-los em grupos para que poder executar os testes juntos. O atributo [Trait] permite organizar os testes em grupos, criando nomes de categoria e atribuindo valores a eles.

        [Fact(DisplayName = "Teste Numero 2")]
        [Trait("Calculo", "Somar")]
        public void Somar_DoisNumeros_RetornaNumero()
        { }

No Test Explorer, este teste aparecerá sob o título Calculo[Somar] (cada combinação Name/Value aparece como um cabeçalho separado no Test Explorer).

Os nomes dos métodos de teste devem ser tão descritivos quanto possível. Na maioria dos casos, é possível nomear o método para que nem seja necessário ler o código real para entender o que está sendo testado.

No exemplo, usamos a seguinte convenção de nomenclatura :

1 - A primeira parte do nome representa o nome do método que está sendo testado;
2-  A segunda parte do nome nos informa mais sobre o cenário de teste;(opcional)
3-  A  última parte do nome é o nome do teste ou o resultado esperado;

Exemplo : Soma_DoisNumerosInteiros_RetornaNumeroInteiro

Temos assim uma apresentação dos principais recursos do XUnit para realização de testes de unidade.

Criando o projeto Web API

Vamos iniciar criando um solução em branco usando o template Blank Solution com o nome MinhaApi.

 

A seguir vamos incluir na solução um projeto pronto e criado com o template ASP.NET Core Web App com o nome MinhaApi.WebApi. Este projeto contem a API que desejamos testar.


Depois disso vamos criar e incluir um novo projeto usando o template xUnit Test Project com o nome MinhaApi.Tests

 


Ao final teremos uma solução com dois projetos:

 

 

Vamos criar as seguintes pastas no projeto Tests:

  1. Controllers

  2. Helpers

  3. MockData

  4. Services

- A pasta Controllers contém os testes relacionados com os Controllers;
- A pasta Helpers contém o código comum usado;
- A pasta MockData contém arquivos relacionados com os dados fake que podem ser usados para o response e request no testes;
- A pasta Services contém os arquivos de testes para o serviço relacionados com a aplicação;

Agora precisamos instalar em nosso projeto MinhaApi.Tests os seguintes pacotes nuget :

Para instalar podemos usar os seguintes comandos:

Ao final teremos os seguintes pacotes instalados em nosso projeto :

Agora inclua uma referência ao projeto de Web API no projeto de testes:

   

Criando os casos de testes

A nossa API representa pelo controlador TarefasController possui o seguinte código :

[Route("api/[controller]")]
[ApiController]
public class TarefasController : ControllerBase
{
    private readonly ITarefaService _tarefaService;
    public TarefasController(ITarefaService tarefaService)
    {
        _tarefaService = tarefaService;
    }

    [Route("tarefas")]
    [HttpGet]

    public async Task<IActionResult> GetTodasTarefasAsync()
    {
        var result = await _tarefaService.GetTarefasAsync();
        if (result.Count == 0)
        {
            return NoContent();
        }
        return Ok(result);
    }

    [HttpPost]
    [Route("novatarefa")]

    public async Task<IActionResult> NovaTarefaAsync(Tarefa novaTarefa)
    {
        await _tarefaService.CriaTarefaAsync(novaTarefa);
        return Ok();
    }
}

Aqui o método GetTodasTarefasAsync() retorna uma coleção de objetos Tarefa.

Vamos iniciar criando dados fictícios para tarefas na pasta MockData do projeto de testes.

Crie a classe TarefasMockData na pasta MockData com o código abaixo:

using MinhaApi.WebApi.Data.Entities;

namespace MinhaApi.Tests.MockData;

public class TarefasMockData
{
    public static List<Tarefa> GetTarefas()
    {
        return new List<Tarefa>{
         new Tarefa{
             Id = 1,
             Nome = "Dar banho no cachorro",
             Concluida = true
         },
         new Tarefa{
             Id = 2,
             Nome = "Continuar leitura do livro",
             Concluida = false
         },
         new Tarefa{
             Id = 3,
             Nome = "Ajustar o código do programa",
             Concluida = false
         }
     };
    }
}

Na pasta Controllers do projeto de testes vamos criar a classe TestTarefaController e a seguir vamos definir casos de testes para o método GetTodasTarefasAsync da API.

1- GetTodasTarefasAsync_ShouldReturn200Status()

public class TestTarefaController
{
    [Fact]
    public async Task GetTodasTarefasAsync_ShouldReturn200Status()
    {
        /// Arrange
        var tarefaService = new Mock<ITarefaService>();
        tarefaService.Setup(_ => _.GetTarefasAsync()).ReturnsAsync(TarefasMockData.GetTarefas());
        var sut = new TarefasController(tarefaService.Object);

        /// Act
        var result = (OkObjectResult)await sut.GetTodasTarefasAsync();

        // /// Assert
        result.StatusCode.Should().Be(200);
    }
}

Esse  é nosso primeiro caso de teste para o método GetTodasTarefasAsync(). Vamos entender o código :

O método é decorado com um atributo como 'Fact', isso determina que o método deve ser executado pelo executor de teste.

A convenção de nomenclatura recomendada do método de teste é 'Nome do método a ser testado' + '
Nome do teste ou o resultado esperado;'.

Aqui podemos observar o código separado com base na fórmula 'AAA'.

- Queremos testar o método 'GetTodasTarefasAsync()' do controlador e sabemos que este método depende do resultado do método 'TarefaService.GetTarefasAsync()', o que significa que o método Action do nosso controlador não se importa com a implementação deste método;

Assim, criamos uma instância simulada de 'ITarefaService' e, em seguida, simulamos o resultado do método 'ITarefaService.GetTarefasAsync()'.

 var tarefaService = new Mock<ITarefaService>();
 tarefaService.Setup(_ => _.GetTarefasAsync()).ReturnsAsync(TarefasMockData.GetTarefas());

Finalmente cria a instância de 'TarefasController'. Aqui a variável 'sut' significa 'System Under Test' apenas uma convenção de nomenclatura recomendada.

 var sut = new TarefasController(tarefaService.Object);

No código invocamos o método Action do controlador 'GetTodasTarefasAsync()', e, como nosso método retorna 'OkObjectResult' para o status 200, aqui fazemos a conversão explicita do resultado :

  var result = (OkObjectResult)await sut.GetTodasTarefasAsync();

A seguir verificamos o resultado esperado que é 200 como código de status.

  result.StatusCode.Should().Be(200);

Acionando o Test Explorer no Visual Studio 2022 podemos verificar o teste criado:

Executando teremos o seguinte resultado:

Vamos escrever mais um caso teste para 'TarefaController.GetTodasTarefasAsync()'  considerando o retorno do status 204 que ocorre quando não há dados para retornar como resposta.

Para isso vamos incluir na classe TarefasMockData um método que retorna uma lista vazia de objetos Tarefa:

public static List<Tarefa> GetTarefasVazia()
{
    return new List<Tarefa>();
}

A seguir vamos criar o caso de teste para o retorno 204 definindo na classe TestTarefaController o método  GetTodasTarefasAsync_ShouldReturn204NoContentStatus :

    [Fact]
    public async Task GetTodasTarefasAsync_ShouldReturn204NoContentStatus()
    {
        /// Arrange
        var tarefaService = new Mock<ITarefaService>();
        tarefaService.Setup(_ => _.GetTarefasAsync()).ReturnsAsync(TarefasMockData.GetTarefasVazia());
        var sut = new TarefasController(tarefaService.Object);

        /// Act
        var result = (NoContentResult)await sut.GetTodasTarefasAsync();

        /// Assert
        result.StatusCode.Should().Be(204);
        tarefaService.Verify(_ => _.GetTarefasAsync(), Times.Exactly(1));
    }

Neste código :

- Estamos mocando o método 'ITarefaSerivice.GetTarefasAsync()' para retornar a coleção de dados vazia.
- Invocamos o método 'TarefaController.GetTodasTarefasAsync()'.
- Esperando que nosso código de status seja '204'
- Na verificação de 'ITarefaService.GetTarefasAsync()' é chamada exatamente uma vez dentro do nosso método 'TarefaConroller.
GetTarefasAsync()'.

Executando no Test Explorer este caso teste obtemos o resultado abaixo:

Na continuação do artigo iremos criar casos de teste para testar o serviço.

"Nada façais por contenda ou por vanglória, mas por humildade; cada um considere os outros superiores a si mesmo."
Filipenses 2:3

Referências:


José Carlos Macoratti