ASP.NET Core - Testes com NUnit  e EF Core in Memory - II


  Neste artigo veremos como realizar testes de unidade para operações CRUD em uma aplicação ASP.NET Core Web API usando o Unit e o EF Core in Memory.

Continuando o artigo anterior vamos criar o projeto de testes, configurá-lo e realizar os testes.

Criando o projeto de Testes de unidade com Unit e EF Core in Memory

Para realizar os testes de unidade vamos usar o NUnit e o Entity Framework Core in Memory.

O NUnit é um popular framework de testes de unidade para a plataforma .NET. Ele fornece uma ampla gama de recursos e funcionalidades para facilitar a criação e execução de testes automatizados. Aqui estão os principais recursos e funções do NUnit:

  1. Atributos de teste: O NUnit usa atributos especiais para identificar métodos de teste. O atributo [Test] é usado para marcar um método como um teste de unidade. Além disso, existem outros atributos como [TestCase], [TestFixture], [SetUp], [TearDown], etc., que permitem uma configuração flexível dos testes.
  2. Asserts: O NUnit oferece uma variedade de métodos de assertiva para verificar o comportamento esperado dos testes. Alguns exemplos comuns incluem Assert.AreEqual, Assert.IsTrue, Assert.IsFalse, Assert.IsNull, Assert.Throws, entre outros. Esses métodos ajudam a comparar valores, verificar exceções lançadas e validar condições.
  3. Test Fixtures: O NUnit permite agrupar testes relacionados em classes de testes chamadas Test Fixtures. Isso facilita a organização e a execução de testes relacionados.
  4. Test Runners: O NUnit oferece suporte a vários runners (executores de testes) para executar testes. Você pode usar a interface de linha de comando do NUnit, a integração com IDEs populares como Visual Studio e Rider, ou usar ferramentas de integração contínua, como o Jenkins, para executar seus testes.
  5. Data-Driven Testing: O NUnit permite a criação de testes orientados a dados usando o atributo [TestCase]. Com essa abordagem, você pode executar o mesmo teste com diferentes conjuntos de dados, facilitando a cobertura de vários cenários.
  6. Configuração e Limpeza: O NUnit oferece os atributos [SetUp] e [TearDown] para configurar e limpar o estado necessário antes e depois da execução dos testes. Isso é útil para preparar o ambiente de teste e garantir que cada teste comece em um estado consistente.
  7. Parametrização de Testes: O NUnit permite que você defina parâmetros para os métodos de teste usando o atributo [ValueSource] ou [Range]. Isso permite que você reutilize um método de teste com diferentes valores de entrada, aumentando a eficiência dos testes.
  8. Categorização de Testes: O NUnit permite categorizar testes usando o atributo [Category]. Isso ajuda a executar um subconjunto específico de testes com base em categorias específicas, como testes de unidade, testes de integração, testes de aceitação, etc.

O Entity Framework In-Memory é uma opção fornecida pelo Entity Framework, uma tecnologia de mapeamento objeto-relacional (ORM) da plataforma .NET, que permite a execução de testes de integração sem a necessidade de um banco de dados real. Em vez disso, ele cria um banco de dados em memória, que é usado exclusivamente para fins de teste.

O Entity Framework In-Memory funciona criando um banco de dados em memória que simula as funcionalidades e comportamentos de um banco de dados real. Ele mantém uma estrutura de dados na memória do computador, permitindo que você execute operações de consulta, inserção, atualização e exclusão como faria em um banco de dados tradicional.

A sua principal vantagem é a velocidade e a simplicidade dos testes de integração. Em vez de depender de um banco de dados real, você pode usar o banco de dados em memória para executar os testes, eliminando a necessidade de configuração e comunicação com um servidor de banco de dados.

Ao utilizar teste recurso, você pode criar um contexto de banco de dados em memória para cada teste, preencher o banco de dados com dados de teste e executar as operações que deseja testar. Após a execução dos testes, o banco de dados em memória é descartado, não afetando o estado de outros testes ou do ambiente de desenvolvimento.

Ao utilizar o Entity Framework Core In-Memory como uma substituição do banco de dados real, estamos isolando o código do banco de dados e poderemos testar apenas a lógica da unidade.

Criando o projeto de Testes

Agora podemos incluir o projeto de testes na nossa solução selecionando a opção File-> New Project e usando o template NUnit Test Project.

Vamos criar o projeto com o nome ApiBlog.Test e a seguir incluir uma referência neste projeto ao projeto ApiBlog usando a opção Add Project Reference e na sequência vamos incluir no projeto os seguintes pacotes nuget:

  1. Microsoft.EntityFrameworkCore.InMemory
  2. FluentAssertions

Você pode usar o comando Install-Package <pacote> ou dotnet add package <pacote>.

A seguir vamos criar no projeto de testes as seguintes pastas de forma a organizar o código:

  • Helpers - Contém recursos e lógica comum ao projeto;
  • Controllers - Contém os arquivos de testes relacionados aos controllers;

Ao final a estrutura do projeto de testes deverá ser a seguinte:

Iniciando os testes de unidade

Vamos criar na pasta Controllers a classe PostsControllerTest onde vamos inicialmente configurar o contexto do EF Core :

using ApiBlog.Repositories;
using ApiBlog.Test.Helpers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace ApiBlog.Test.Controllers;
public class PostsControllerTest
{
    private static DbContextOptions<AppDbContext> dbContextOptions =
                                                 new DbContextOptionsBuilder<AppDbContext>()
                                                .UseInMemoryDatabase(databaseName: "BlogDb")
                                                .Options;
    AppDbContext context;
    private PostRepository _postRepository;
    [OneTimeSetUp]
    public void Setup()
    {
        context = new AppDbContext(dbContextOptions);
        context.Database.EnsureCreated();
        SeedDataInitial.Seed(context);
        _postRepository = new PostRepository(context);
    }
    [OneTimeTearDown]
    public void CleanUp()
    {
        context.Database.EnsureDeleted();
    }   
}

Este código usa o NUnit e o EF Core In-Memory para configurar o ambiente e testar os endpoints de da API ApiBlog. Vamos entender o código:

  1. private static DbContextOptions<AppDbContext> dbContextOptions: É uma variável estática que armazena as opções de configuração do contexto do banco de dados. Nesse caso, está configurado para usar o banco de dados em memória com o nome "BlogDb".
     
  2. AppDbContext context;: É uma instância do contexto do banco de dados AppDbContext. Essa instância será usada nos testes para acessar o banco de dados em memória.
     
  3. private PostRepository _postRepository;: É uma instância do PostRepository, que é o repositório usado para acessar os dados relacionados aos posts.
     
  4. [OneTimeSetUp]: É um atributo do NUnit que indica que o método Setup() será executado uma vez antes de todos os testes nesta classe. Esse método é usado para configurar o ambiente de teste.
     
  5. context = new AppDbContext(dbContextOptions);: Cria uma nova instância do contexto do banco de dados AppDbContext usando as opções de configuração definidas em dbContextOptions.
     
  6. context.Database.EnsureCreated();: Garante que o banco de dados em memória seja criado antes de executar os testes. Isso cria o esquema do banco de dados e aplica as migrações pendentes.
     
  7. SeedDataInitial.Seed(context);: Chama o método Seed() da classe SeedDataInitial para inserir dados iniciais no banco de dados em memória. Isso é útil para ter dados de teste predefinidos para executar os testes.
     
  8. _postRepository = new PostRepository(context);: Cria uma instância do PostRepository, passando o contexto do banco de dados AppDbContext como argumento. Essa instância será usada nos testes para acessar os dados dos posts.
     
  9. [OneTimeTearDown]: É um atributo do NUnit que indica que o método CleanUp() será executado uma vez após todos os testes nesta classe. Esse método é usado para limpar o ambiente de teste.
     
  10. context.Database.EnsureDeleted();: Garante que o banco de dados em memória seja excluído após a execução dos testes. Isso garante que o ambiente de teste seja limpo e não deixe nenhum resíduo.

Precisamos criar a classe SeedDataInitial que fornece os dados iniciais em memória para realizar os testes. Assim na pasta Helpers vamos criar a classe SeedDataInitial com o código abaixo:

public class SeedDataInitial
{
    public static void Seed(AppDbContext context)
    {
        context.Categorias.AddRange(
            new Categoria() { Nome = "CSHARP", Chave = "csharp" },
            new Categoria() { Nome = "VISUAL STUDIO", Chave = "visualstudio" },
            new Categoria() { Nome = "ASP.NET CORE", Chave = "aspnetcore" },
            new Categoria() { Nome = "SQL SERVER", Chave = "sqlserver" }
        );
        context.Posts.AddRange(
            new Post() { Titulo = "ASP.NET Core - CRUD", Descricao = "CRUD com EF Core ", CategoriaId = 1, DataCriacao = DateTime.Now },
            new Post() { Titulo = "C# - Novidades ", Descricao = "Novidades do C# 10", CategoriaId = 3, DataCriacao = DateTime.Now },
            new Post() { Titulo = "SQL Server - Funcões básicas", Descricao = "As funções básicas do SQL Server" ,CategoriaId = 4, DataCriacao = DateTime.Now },
            new Post() { Titulo = "Visual Studio 2022 - Novos recursos", Descricao = "Os novos recursos do VS 2022", CategoriaId = 2, DataCriacao = DateTime.Now }
        );
        context.SaveChanges();
    }
}

Agora podemos iniciar criando os métodos de testes usando o NUnit.

Vamos iniciar testando o retorno de um Post pelo seu Id considerando 3 cenários:

  1. Ok(post)  - O post foi encontrado usando o Id informado(Id igual a 1);
  2. NotFound() - O não foi encontrado usando o Id Informado(Id igual a 9);
  3. BadRequest() - Ocorreu um BadRequest no request (testar com null);

Abaixo temos os 3 métodos de testes para estes cenários :

    [Test]
    public async Task Task_GetPostById_Return_OkResult()

    {
        // Arrange
        var controller = new PostsController(_postRepository);

        // Act
        var result = await controller.GetPost(1);

        // Assert
        Assert.IsInstanceOf<OkObjectResult>(result);
        var okResult = result as OkObjectResult;
        Assert.IsInstanceOf<PostDto>(okResult.Value);
        var postDto = okResult.Value as PostDto;
        Assert.That(postDto.PostId, Is.EqualTo(1));

    }

    [Test]
    public async Task Task_GetPostById_Return_NotFoundResult()

    {
        // Arrange
        var controller = new PostsController(_postRepository);

        // Act
        var result = await controller.GetPost(9);

        // Assert
        Assert.IsInstanceOf<NotFoundResult>(result);
        var notFoundResult = result as NotFoundResult;
        Assert.That(notFoundResult.StatusCode, Is.EqualTo(404));
    }

    [Test]
    public async Task Task_GetPosts_Return_BadRequestResult()

    {
        // Arrange
        var controller = new PostsController(_postRepository);

        // Act
        var result = await controller.GetPost(null);

        // Assert
        Assert.IsInstanceOf<BadRequestResult>(result);
        var badRequestResult = result as BadRequestResult;
        Assert.That(badRequestResult.StatusCode, Is.EqualTo(400));
    }
 

Entendendo o código:

  • O atributo [Test] indica que o método é um caso de teste no NUnit.
  • O bloco Arrange é usado para configurar o ambiente de teste,
  • O bloco Act é usado para realizar a ação de teste,
  • O bloco Assert é usado para verificar os resultados do teste.
  • No método Task_GetPostById_Return_OkResult(), podemos chamar o método GetPost() do PostsController passando um ID válido do post que você deseja buscar. Em seguida, podemos verificar o tipo de retorno e assegurar que o objeto retornado seja do tipo PostDto com o ID esperado.

    Certifique-se de adaptar o ID do post que você deseja buscar e adicionar asserções adicionais conforme necessário para verificar outros atributos do PostDto.

    Essas etapas devem permitir testar o retorno de um post pelo seu ID usando o NUnit e o EF Core In-Memory. Lembre-se de que já configuramos o contexto do banco de dados em memória no método de configuração Setup() da classe de teste e inseriu dados de teste usando o método SeedDataInitial.Seed(context).

    No método Task_GetPostById_Return_NotFoundResult() para testar o cenário em que o post não é encontrado e o retorno é "Not Found", você pode usar o método Assert.IsInstanceOf() do NUnit para verificar se o tipo de retorno é NotFoundResult. Além disso, você também pode verificar o código de status HTTP da resposta para garantir que seja 404 (Not Found).

    Para testar o cenário em que o controlador retorna um BadRequest (400), podemos usar o método Assert.IsInstanceOf() do NUnit para verificar se o tipo de retorno é BadRequestResult. Além disso,  podemos verificar o código de status HTTP da resposta para garantir que seja 400 (BadRequest).

    Abaixo vemos a janela do Test Explorer exibindo o resultado da execução destes testes:

    A seguir temos o método de testes para o método Action do controlador que retorna todos os Posts:

        [Test]
        public async Task Task_GetPosts_MatchResult()

        {
            // Arrange
            var controller = new PostsController(_postRepository);

            // Act
            var data = await controller.GetPosts();

            // Assert
            Assert.IsInstanceOf<OkObjectResult>(data);

            var okResult = data.Should().BeOfType<OkObjectResult>().Subject;
            var posts = okResult.Value.Should().BeAssignableTo<List<PostDto>>().Subject;

            Assert.That(posts[0].Titulo, Is.EqualTo("ASP.NET Core - CRUD"));
            Assert.That(posts[0].Descricao, Is.EqualTo("CRUD com EF Core "));

            Assert.That(posts[1].Titulo, Is.EqualTo("C# - Novidades "));
            Assert.That(posts[1].Descricao, Is.EqualTo("Novidades do C# 10"));
        }

    Entendendo o código acima:

    1. O atributo [Test] indica que o método é um caso de teste no NUnit.
    2. O bloco Arrange é usado para configurar o ambiente de teste, onde você instancia um objeto PostsController passando o _postRepository como argumento.
    3. O bloco Act é usado para realizar a ação de teste, onde você chama o método GetPosts do controlador e atribui o resultado à variável data. Neste caso, espera-se que o método retorne uma lista de posts.
    4. O bloco Assert é usado para verificar os resultados do teste.
      • Assert.IsInstanceOf<OkObjectResult>(data) verifica se o objeto data é uma instância de OkObjectResult, o que indica que a requisição obteve sucesso.
      • data.Should().BeOfType<OkObjectResult>().Subject utiliza o FluentAssertions para realizar a mesma verificação anterior.
      • okResult.Value.Should().BeAssignableTo<List<PostDto>>().Subject verifica se o valor do okResult pode ser atribuído a uma lista de PostDto.
      • Assert.That(posts[0].Titulo, Is.EqualTo("ASP.NET Core - CRUD")) compara se o título do primeiro post é igual a "ASP.NET Core - CRUD" utilizando a sintaxe de asserção do NUnit.
      • Assert.That(posts[0].Descricao, Is.EqualTo("CRUD com EF Core ")) compara se a descrição do primeiro post é igual a "CRUD com EF Core " utilizando a sintaxe de asserção do NUnit.
      • Assert.That(posts[1].Titulo, Is.EqualTo("C# - Novidades ")) compara se o título do segundo post é igual a "C# - Novidades " utilizando a sintaxe de asserção do NUnit.
      • Assert.That(posts[1].Descricao, Is.EqualTo("Novidades do C# 10")) compara se a descrição do segundo post é igual a "Novidades do C# 10" utilizando a sintaxe de asserção do NUnit.

    Essas asserções são usadas para verificar se os resultados obtidos do método GetPosts estão corretos, comparando os títulos e descrições dos posts retornados.

    Agora vamos criar o método de teste para testar a criação de um novo Post usando o método Action  addPost do controlador:

        [Test]
        public async Task Task_Add_ValidData_Return_OkResult()

        {
            //Arrange
            var controller = new PostsController(_postRepository);
            var post = new Post() { Titulo = "Teste Titulo 3", Descricao = "Teste Descrição 3",
                                          CategoriaId = 2, DataCriacao = DateTime.Now };

            //Act
            var data = await controller.AddPost(post);

            // Assert
            Assert.IsInstanceOf<OkObjectResult>(data);
        }

    Este código atua da seguinte forma:

    1. O atributo [Test] indica que o método é um caso de teste no NUnit.
       
    2. O bloco Arrange é usado para configurar o ambiente de teste. Nele, você instancia um objeto PostsController passando o _postRepository como argumento. Além disso, você cria um objeto Post com dados válidos para adicionar.
       
    3. O bloco Act é usado para executar a ação de teste, onde você chama o método AddPost do controlador, passando o objeto post como argumento, e atribui o resultado à variável data. Neste caso, espera-se que o método retorne um resultado indicando sucesso (200 OK).
       
    4. O bloco Assert é usado para verificar o resultado do teste.
      • Assert.IsInstanceOf<OkObjectResult>(data) verifica se o objeto data é uma instância de OkObjectResult, o que indica que a operação de adição foi bem-sucedida e retornou um resultado do tipo 200 OK.

    Agora veremos o método de teste usado para testar a atualização de um Post :

        [Test]
        public async Task Task_Update_ValidData_Return_OkResult()
        {
            // Arrange
            var controller = new PostsController(_postRepository);
            var postId = 2;
            // Act
            var existingPost = await controller.GetPost(postId);
            var okResult = existingPost.Should().BeOfType<OkObjectResult>().Subject;
            var result = okResult.Value.Should().BeAssignableTo<PostDto>().Subject;
            var post = new Post();
            post.Titulo = "Novidades do C# - Atualizado(Teste)";
            post.Descricao = result.Descricao;
            post.CategoriaId = result.CategoriaId;
            post.DataCriacao = result.DataCriacao;
            var updatedData = await controller.UpdatePost(post);
            // Assert
            Assert.IsInstanceOf<OkResult>(updatedData);
        }

    Vejamos o que faz este código :

  • var controller = new PostsController(_postRepository): Cria uma instância do controlador PostsController passando o _postRepository como parâmetro.
  • var postId = 2;: Define o ID de um post existente que será utilizado no teste.
  • var existingPost = await controller.GetPost(postId): Chama o método GetPost do controlador para obter um post existente com o ID especificado.
  • var okResult = existingPost.Should().BeOfType<OkObjectResult>().Subject: Verifica se o resultado da chamada é um OkObjectResult usando o FluentAssertions. Se for do tipo esperado, o okResult recebe o resultado.
  • var result = okResult.Value.Should().BeAssignableTo<PostDto>().Subject: Verifica se o valor do OkObjectResult é atribuível a um objeto PostDto usando o FluentAssertions. Se for atribuível, o result recebe o valor.
  • var post = new Post() { ... }: Cria um novo objeto Post com os dados atualizados que serão usados para atualizar o post existente.
  • var updatedData = await controller.UpdatePost(post): Chama o método UpdatePost do controlador para atualizar o post com os dados fornecidos.
  • Assert.IsInstanceOf<OkResult>(updatedData): Verifica se o resultado da atualização é do tipo OkResult. Se for, o teste passa.
  • Para concluir veremos o método de teste para testar a exclusão de um Post.

        [Test]
        public async Task Task_Delete_Post_Return_OkResult()
        {
            //Arrange
            var controller = new PostsController(_postRepository);
            var postId = 2;
            //Act
            var data = await controller.DeletePost(postId);
            //Assert
            Assert.IsInstanceOf<OkResult>(data);
            // Podemos usar FuentAssertions assim
            data.Should().BeOfType<OkResult>();
        }

    Aqui, estamos criando uma instância do PostsController e passando o _postRepository como dependência. Em seguida, chamamos o método DeletePost do controlador, fornecendo o ID do post que desejamos excluir.

    No bloco de Assert, estamos verificando se o resultado retornado da exclusão do post é do tipo OkResult.

    Usamos o método Assert.IsInstanceOf<T> para essa verificação. Também podemos usar o FluentAssertions para obter uma sintaxe mais expressiva, usando data.Should().BeOfType<OkResult>() para realizar a mesma verificação.

    Concluímos assim a implementação dos testes para o CRUD usando NUnit e o EF Core in Memory com ajuda dasa FluentAssertions. Você pode expandir os métodos de teste realizando testes para outros cenários.

    Pegue o projeto aqui:   ApiBlog.Test.zip (sem as referências)

    "Bendito seja o Deus e Pai de nosso Senhor Jesus Cristo que, segundo a sua grande misericórdia, nos gerou de novo para uma viva esperança, pela ressurreição de Jesus Cristo dentre os mortos"
    1 Pedro 1:3

    Referências:


    José Carlos Macoratti