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


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

Os testes de unidade são testes automatizados que visam testar uma unidade de código de forma isolada, ou seja, sem depender de outras partes do sistema.

O objetivo é garantir que cada parte do código esteja funcionando corretamente, detectar erros antes que eles afetem outras partes do sistema e permitir que os desenvolvedores refatorem o código com segurança.

O xUnit é um framework de teste de unidade open source para a plataforma .NET, criado por James Newkirk e Brad Wilson.(Ele é inspirado no JUnit, um popular framework de teste para a plataforma Java.)

O xUnit funciona criando classes de teste, que contêm métodos que realizam testes específicos, onde cada método de teste deve seguir um padrão de nomeação específico, por exemplo, "TestMétodoAcaoResultadoEsperado".

O framework também possui diversos atributos e métodos auxiliares que permitem a configuração e a verificação dos testes. Os principais recursos do xUnit incluem:

  1. Atributos de teste: O xUnit fornece diversos atributos para controlar o comportamento dos testes, como [Fact], [Theory], [InlineData], [Skip], [Trait], entre outros.
  2. Asserts: O xUnit possui diversos métodos assert que permitem verificar se o resultado esperado do teste foi alcançado. Alguns exemplos incluem Assert.Equal, Assert.True, Assert.False, Assert.Throws, entre outros.
  3. Teoria de dados: O xUnit suporta a teoria de dados, que permite testar uma mesma unidade de código com diferentes conjuntos de dados de entrada.
  4. Configuração e limpeza: O xUnit fornece métodos que podem ser executados antes e depois de cada teste ou de todos os testes, permitindo a configuração e limpeza do ambiente de teste.
  5. Paralelismo: O xUnit suporta a execução de testes em paralelo, permitindo que vários testes sejam executados simultaneamente para acelerar a execução dos testes.
  6. Extensibilidade: O xUnit é extensível, permitindo que os usuários criem seus próprios atributos, assert methods e outras extensões.
  7. Integração com ferramentas de build: O xUnit pode ser facilmente integrado com ferramentas de build como o MSBuild, o Visual Studio, o dotnet CLI, entre outras.

O xUnit é um framework popular para testes de unidade na plataforma .NET, devido à sua facilidade de uso, flexibilidade e recursos avançados. Ele é usado por desenvolvedores e equipes de desenvolvimento em todo o mundo para garantir a qualidade do código e aumentar a confiança de que o código está funcionando corretamente.

Vamos criar um projeto ASP.NET Core Web API usando o .NET 7.0 onde vamos realizar o CRUD básico para gerenciar informações dos posts de um Blog.

recursos usados:

  • .NET 7.0
  • SQL Server
  • EF Core 7.0

Criando e configurando o projeto

Vamos criar um projeto ASP.NET Core Web API chamado ApiBlog usando o VS 2022 usando o EF Core na abordagem Code-First.

A seguir inclua no projeto os seguintes pacotes nuget:

  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools

Crie a pasta Entities no projeto e nesta pasta crie a classe Post com o seguinte código:

public class Post
{
    public int PostId { get; set; }
    public string? Titulo { get; set; }
    public string? Descricao { get; set; }
    public DateTime DataCriacao { get; set; }

    public int CategoriaId { get; set; }
    public Categoria? Categoria { get; set; }
}

A seguir vamos criar na mesma pasta a classe Categoria:

public class Categoria
{
    public Categoria()
    {
        Post = new HashSet<Post>();
    }
    public int Id { get; set; }
    public string? Nome { get; set; }
    public string? Chave { get; set; }
    public ICollection<Post>? Post { get; set; }
}

A seguir crie a pasta Context e nela crie a classe AppDbContext que herda da classe DbContext do EF Core:

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options)
    { }

    public virtual DbSet<Categoria> Categorias { get; set; }
    public virtual DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Categoria>(entity =>
        {
            entity.Property(e => e.Id)
                  .HasColumnName("Id");

            entity.Property(e => e.Nome)
                .HasColumnName("Nome")
                .HasMaxLength(100)
                .IsUnicode(false);

            entity.Property(e => e.Chave)
                .HasColumnName("Chave")
                .HasMaxLength(50)
                .IsUnicode(false);
        });

        modelBuilder.Entity<Post>(entity =>
        {
            entity.Property(e => e.PostId)
                  .HasColumnName("PostId");

            entity.Property(e => e.CategoriaId)
                  .HasColumnName("CategoriaId");

            entity.Property(e => e.DataCriacao)
                .HasColumnName("DataCriacao")
                .HasColumnType("datetime");

            entity.Property(e => e.Descricao)
                .HasColumnName("Descricao")
                .HasMaxLength(100)
                .IsUnicode(false);

            entity.Property(e => e.Titulo)
                .HasColumnName("Titulo")
                .HasMaxLength(100)
                .IsUnicode(false);

            entity.HasOne(d => d.Categoria)
                .WithMany(p => p.Post)
                .HasForeignKey(d => d.CategoriaId);
        });
    }
}

O arquivo de contexto define o mapeamento da entidade para a tabela do SQL Server e no método OnModelCreating estamos definindo o mapeamento ORM das entidades.

Vamos definir no arquivo appsettings.json a string de conexão com o SQL Server:

{
  
"ConnectionStrings": {
    
"DefaultConnection": "Server=.;Database=BlogDB;Trusted_Connection=True;TrustServerCertificate=True;"
},
...

Como estamos usando o  EF Core 7.0 temos que incluir a propriedade TrustServerCertificate=True; na string de conexão.

Agora vamos registrar o serviço do contexto no Contêiner DI na classe Program:

using Microsoft.EntityFrameworkCore;

var
builder = WebApplication.CreateBuilder(args);
// Add services to the container.

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddCors(option => option.AddPolicy("MeuBlogPolicy", builder =>
{
   builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod();
}));

builder.Services.AddDbContext<AppDbContext>(options =>  options.UseSqlServer   
     (builder.Configuration.GetConnectionString(
"DefaultConnection")));

builder.Services.AddScoped<IPostRepository, PostRepository>();

var app = builder.Build();

...
app.UseHttpsRedirection();
app.UseAuthorization();

app.UseCors(
"MeuBlogPolicy");

app.MapControllers();
app.Run();

A seguir podemos aplicar o Migrations usando os seguintes comandos na sequência :

  • add-migration Inicial
  • update-database

Ao final teremos o banco BlogDB criado no SQL Server e contendo as tabelas Posts e Categorias :

Criando o Repositório

Vamos criar no projeto a pasta Repositories e nesta pasta implementar o repositório de Posts que vão conter a lógica de negócios para todas as nossas operações relacionadas aos posts.

Vamos criar a classe PostDto na pasta DTOs para realizar a transferência de dados :

public class PostDto
{
  
public int PostId { get; set; }
  
public string? Titulo { get; set; }
  
public string? Descricao { get; set; }
  
public int CategoriaId { get; set; }
  
public DateTime DataCriacao { get; set; }
  
public string? CategoriaNome { get; set; }
}

A seguir vamos criar a interface IPostRepository:

public interface IPostRepository
{
  Task<List<Categoria>> GetCategorias();
  Task<List<PostDto>> GetPosts();
  Task<PostDto> GetPost(
int? postId);
  Task<
int> AddPost(Post post);
  Task<
int> DeletePost(int? postId);
  Task UpdatePost(Post post);
}

A seguir temos a classe concreta PostRepository que implementa esta interface :

public class PostRepository : IPostRepository
{
    private readonly AppDbContext db;
    public PostRepository(AppDbContext _db)
    {
        db = _db;
    }

    public async Task<List<Categoria>> GetCategorias()
    {
        if (db != null)
        {
            return await db.Categorias.ToListAsync();
        }

        return null;
    }

    public async Task<List<PostDto>> GetPosts()
    {
        if (db is not null)
        {
            return await (from p in db.Posts
                          from c in db.Categorias
                          where p.CategoriaId == c.Id
                          select new PostDto
                          {
                              PostId = p.PostId,
                              Titulo = p.Titulo,
                              Descricao = p.Descricao,
                              CategoriaId = p.CategoriaId,
                              CategoriaNome = c.Nome,
                              DataCriacao = p.DataCriacao
                          }).ToListAsync();

        }

        return null;
    }

    public async Task<PostDto> GetPost(int? postId)
    {
        if (db is not null)
        {
            return await (from p in db.Posts
                          from c in db.Categorias
                          where p.PostId == postId
                          select new PostDto
                          {
                              PostId = p.PostId,
                              Titulo = p.Titulo,
                              Descricao = p.Descricao,
                              CategoriaId = p.CategoriaId,
                              CategoriaNome = c.Nome,
                              DataCriacao = p.DataCriacao
                          }).FirstOrDefaultAsync();

        }

        return null;
    }

    public async Task<int> AddPost(Post post)
    {
        if (db is not null)
        {
            await db.Posts.AddAsync(post);
            await db.SaveChangesAsync();

            return post.PostId;
        }

        return 0;
    }

    public async Task<int> DeletePost(int? postId)
    {
        int result = 0;

        if (db is not null)
        {
            var post = await db.Posts.FirstOrDefaultAsync(x => x.PostId == postId);

            if (post != null)
            {
                //Deleta o post
                db.Posts.Remove(post);
                result = await db.SaveChangesAsync();
            }
            return result;
        }
        return result;
    }

    public async Task UpdatePost(Post post)
    {
        if (db is not null)
        {
            //atualiza o post
            db.Posts.Update(post);
            await db.SaveChangesAsync();
        }
    }
}

Para concluir vamos criar na pasta Controllers o controlador PostsController com o código  abaixo:

using ApiBlog.Entities;
using ApiBlog.Repositories;
using Microsoft.AspNetCore.Mvc;

namespace ApiBlog.Controllers;

[Route("api/[controller]")]
[ApiController]
public class PostsController : ControllerBase
{
    private readonly IPostRepository postRepository;
    public PostsController(IPostRepository _postRepository)
    {
        postRepository = _postRepository;
    }

    [HttpGet]
    [Route("GetCategories")]
    public async Task<IActionResult> GetCategories()
    {
        try
        {
            var categories = await postRepository.GetCategorias();
            if (categories == null)
            {
                return NotFound();
            }
            return Ok(categories);
        }
        catch (Exception)
        {
            return BadRequest();
        }
    }

    [HttpGet]
    [Route("GetPosts")]
    public async Task<IActionResult> GetPosts()
    {
        try
        {
            var posts = await postRepository.GetPosts();
            if (posts == null)
            {
                return NotFound();
            }
            return Ok(posts);
        }
        catch (Exception)
        {
            return BadRequest();
        }
    }

    [HttpGet]
    [Route("GetPost")]
    public async Task<IActionResult> GetPost(int? postId)
    {
        if (postId == null)
        {
            return BadRequest();
        }

        try
        {
            var post = await postRepository.GetPost(postId);

            if (post == null)
            {
                return NotFound();
            }

            return Ok(post);
        }
        catch (Exception)
        {
            return BadRequest();
        }
    }

    [HttpPost]
    [Route("AddPost")]
    public async Task<IActionResult> AddPost([FromBody] Post model)
    {
        if (ModelState.IsValid)
        {
            try
            {
                var postId = await postRepository.AddPost(model);
                if (postId > 0)
                {
                    return Ok(postId);
                }
                else
                {
                    return NotFound();
                }
            }
            catch (Exception)
            {
                return BadRequest();
            }
        }
        return BadRequest();
    }

    [HttpPost]
    [Route("DeletePost")]
    public async Task<IActionResult> DeletePost(int? postId)
    {
        int result = 0;

        if (postId == null)
        {
            return BadRequest();
        }

        try
        {
            result = await postRepository.DeletePost(postId);
            if (result == 0)
            {
                return NotFound();
            }
            return Ok();
        }
        catch (Exception)
        {
            return BadRequest();
        }
    }

    [HttpPost]
    [Route("UpdatePost")]
    public async Task<IActionResult> UpdatePost([FromBody] Post model)
    {
        if (ModelState.IsValid)
        {
            try
            {
                await postRepository.UpdatePost(model);

                return Ok();
            }
            catch (Exception ex)
            {
                if (ex.GetType().FullName ==
                    "Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException")
                {
                    return NotFound();
                }
                return BadRequest();
            }
        }

        return BadRequest();
    }
}

Com isso temos nossa API pronta para ser usada e executando o projeto teremos os endpoints exibidos na interface do Swagger como mostra a figura:

Na próxima parte do artigo vamos criar o projeto de testes usando o NUnit.

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

"Porque Deus, que disse que das trevas resplandecesse a luz, é quem resplandeceu em nossos corações, para iluminação do conhecimento da glória de Deus, na face de Jesus Cristo"
2 Coríntios 4:6

Referências:


José Carlos Macoratti