ASP .NET Core API - Testes de unidade : Roteiro básico - II


 Hoje vamos continuar a realização dos testes de unidade em um projeto ASP .NET Core API que realiza um CRUD.

Continuando a primeira parte do artigo vamos agora iniciar os nossos testes de unidade.


O teste unitário é a metodologia de teste mais popular e ajuda a testar cada pequena unidade do código-fonte sendo usado para simular a dependência do código para que a unidade individual do código seja testada separadamente sem incluir a dependência no código de teste.

Normalmente, em um aplicativo de software, a unidade é uma classe ou um método em uma classe. Se a classe tiver alguma dependência, para testar a unidade do código, simulamos a dependência para obter um acoplamento fraco. O Teste Unitário pode ser feito por um desenvolvedor e o desenvolvedor pode testar a qualidade do código.

Além dos testes de unidade temos os testes de integração onde as unidades individuais são testadas como um grupo e os testes de ponta a ponta que testa todo o sistema.

Iniciando os testes de unidade

Vamos recordar os endpoints da nossa API para sabermos exatamente o que precisamos testar:

GET /Livros Retorna todos os livros
GET /Livros/{id} Obtêm um livro pelo id
POST /Livros Adiciona um novo livro
PUT /Livros/{id} Atualiza um livro existente      
DELETE /Livros/{id}     Deleta um livro

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.

Ao escrever testes unitários, em geral seguimos o princípio AAA :  Act, Arrange e Assert (Organizar, Agir e Assertir):

Arrange - É aqui que você normalmente prepara tudo para o teste, em outras palavras, prepara a cena para testar (criar os objetos e configurá-los conforme necessário)

Act - É onde o método que estamos testando será executado;

Assert - Esta é a parte final do teste em que comparamos o que esperamos que aconteça com o resultado real da execução do método de teste;

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 HTTP  que está sendo testado;
2-  A segunda parte do nome nos informa mais sobre o cenário de teste;
3-  A  última parte do nome é o resultado esperado;

Exemplo : GET_Livros_RetornaListaLivros

1- Testando o método GET que retorna todos os itens : GET_Livros_RetornaOKListaLivros

using ApiLivraria.Controllers;
using ApiLivraria.Models;
using ApiLivraria.Services;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using Xunit;

namespace ApiLivraria.Tests;

public class ApiLivrariaTest
{
    LivrosController _controller;
    ILivroService _service;

    public ApiLivrariaTest()
    {
        _service = new LivroService();
        _controller = new LivrosController(_service);

    }

    [Fact]
    public void
GET_Livros_RetornaOKListaLivros()
    {
        //Arrange
        //Act

        var result = _controller.Get();

        //Assert
        Assert.IsType<OkObjectResult>(result.Result);

        var lista = result.Result as OkObjectResult;
        Assert.IsType<List<Livro>>(lista.Value);

        var listaLivros = lista.Value as List<Livro>;
        Assert.Equal(5, listaLivros.Count);
    }
}
 

Neste código temos que :

1- Definimos as referências no construtor para o serviço LivroService() e para o controlador LivrosController() que tem como parâmetro o serviço.

A seguir vamos testar o primeiro endpoint GET que deve retornar uma lista de livros usando o método Ok().

Assim não temos que passar nenhum parâmetro em LivrosControllerTest pois o método Get não recebe parâmetros apenas retorna todos os livros.

Assim a definição em :

A primeira coisa que verificamos e se o tipo de resposta é um OkObjectResult, então, uma vez que sabemos que o endpoint da API retornou um objeto Ok, verificamos o tipo de resultado, e vemos se é uma lista de livros.

Então, ao final, verificamos o número de livros que foram devolvidos e assim testamos com sucesso o método Get do nosso Controller usando o método GET_Livros_RetornaOKListaLivros.

Acionando o menu Test -> Test Explorer no Visual Studio e executando o teste teremos o resultado abaixo:

2- Testando o método GET que retorna um livro pelo id : GET_Livro_RetornaOKListaLivros

No código omitimos a definição do serviço e do controlador e focamos apenas no código usado pelo método para testar a obtenção de um livro pelo seu Guid.

Aqui vamos usar o atributo [Theory] que indica que temos 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.

Vamos passar dois valores de Guid : Um válido e que vai retornar o primeiro livro e outro inválido que vai retornar NotFound.

    [Theory]
    [InlineData("ab2bd817-98cd-4cf3-a80a-53ea0cd9c200", "ab2bd817-98cd-4cf3-a80a-53ea0cd9c111")]

    public void GET_LivroGuid_RetornaOKLivro(string guid1, string guid2)
    {
        //Arrange
        var guidValido = new Guid(guid1);
        var guidInvalido = new Guid(guid2);

        //Act
        var notFoundResult = _controller.Get(guidInvalido);
        var okResult = _controller.Get(guidValido);

        //Assert
        Assert.IsType<NotFoundResult>(notFoundResult.Result);
        Assert.IsType<OkObjectResult>(okResult.Result);

        //Precisamos checar o valor do resultado para método OK
        var item = okResult.Result as OkObjectResult;

        //Esperamos retornar um livro
        Assert.IsType<Livro>(item.Value);

        //Vamos verificar o valor
        var livro = item.Value as Livro;
        Assert.Equal(guidValido, livro.Id);
        Assert.Equal("O Gerente", livro.Titulo);

    }

Ao final estamos conferindo também se o título do livro confere com o título do primeiro livro que é o livro esperado para o GUID válido informado.

3- Testando o método POST que cria um novo livro : POST_Livro_CreatedAtLivro

Agora vamos criar um novo livro com dados completos e verificar o resultado que deve ser um CreatedAtActionResult e o valor e a seguir vamos testar a criação de um livro incompleto onde teremos um BadRequest  com estado inválido.

    [Fact]
    public void POST_Livro_CreatedAtLivro()
    {
        //Arrange
        var NovoLivro = new Livro()
        {
            Autor = "Macoratti",
            Titulo = "Meu Livro",
        };

        //Act
        var createdResponse = _controller.Post(NovoLivro);

        //Assert
        Assert.IsType<CreatedAtActionResult>(createdResponse);

        //valor do resultado
        var item = createdResponse as CreatedAtActionResult;
        Assert.IsType<Livro>(item.Value);

        //verificar o valor do livro
        var livro = item.Value as Livro;
        Assert.Equal(NovoLivro.Autor, livro.Autor);
        Assert.Equal(NovoLivro.Titulo, livro.Titulo);

        //OK RESULT TESTE termina

        //BADREQUEST e ERRO de MODELSTATE O test reinicia
        //

        //Arrange
        var livroIncompleto = new Livro()
        {
            Autor = "Macoratti",
        };

        //Act
        _controller.ModelState.AddModelError("Titulo", "O titulo é obrigatório");
        var badResponse = _controller.Post(livroIncompleto);

        //Assert
        Assert.IsType<BadRequestObjectResult>(badResponse);
    }

Executando o teste teremos o resultado abaixo:

4- Testando o método DELETE que excluir um livro pelo GUID : DELETE_LivroGUID_OK

Agora iremos testar o método DELETE para excluir um livro pelo GUID.

Vamos informar dois guids, um válido e outro inválido usando os atributos [Theory] e {InlineData()].

A seguir vamos obter um NotFound para o guid inválido e assim o total de livros deverá continuar igual a 5.

Depois vamos excluir um livro com GUID válido e vamos obter OK e teremos um livro a menos no total.

    [Theory]
    [InlineData("ab2bd817-98cd-4cf3-a80a-53ea0cd9c200", "ab2bd817-98cd-4cf3-a80a-53ea0cd9c111")]
    public void DELETE_LivroGUID_OK(string guid1, string guid2)
    {
        //Arrange
        var guidValido = new Guid(guid1);
        var guidInvalido = new Guid(guid2);

        //Act
        var notFoundResult = _controller.Remove(guidInvalido);

        //Assert
        Assert.IsType<NotFoundResult>(notFoundResult);
        Assert.Equal(5, _service.GetAll().Count());

        //Act
        var okResult = _controller.Remove(guidValido);

        //Assert
        Assert.IsType<OkResult>(okResult);
        Assert.Equal(4, _service.GetAll().Count());
    }

Executando o teste teremos o resultado abaixo:

Concluímos assim os nossos testes de unidade que foram feitos de forma bem simples onde apresentamos alguns aspectos e usos do xUnit.

Pegue o projeto aqui:  ApiLivraria.zip (sem referências)

"Mas agora, morrendo para aquilo que antes nos prendia, fomos libertados da lei, para que sirvamos conforme o novo modo do Espírito, e não segundo a velha forma da lei escrita."
Romanos 7:6

Referências:


José Carlos Macoratti