Pular para conteúdo

Testes do Backend

Estrategia e estrutura de testes do backend .NET do TepConfina.

Framework e Ferramentas

Ferramenta Versao Funcao
xUnit 2.x Framework de testes
NSubstitute 5.x Biblioteca de mocking
FluentAssertions 8.x Assertions expressivas
WebApplicationFactory - Testes de integracao ASP.NET Core

Estrutura do Projeto

tests/
  TepConfina.UnitTests/
    Services/
      LoteServiceTests.cs
      AnimalServiceTests.cs
      PesagemServiceTests.cs
      RacaoServiceTests.cs
      MedicamentoServiceTests.cs
      OutrosGastosServiceTests.cs
  TepConfina.IntegrationTests/
    CustomWebApplicationFactory.cs
    AuthHelper.cs
    Controllers/
      AuthControllerTests.cs
      LotesControllerTests.cs
      AnimaisControllerTests.cs
      ...

Testes Unitarios (28 testes)

Padrao Arrange-Act-Assert

Todos os testes seguem o padrao AAA:

public class LoteServiceTests
{
    private readonly ILoteRepository _loteRepository;
    private readonly ICurrentUserProvider _currentUserProvider;
    private readonly LoteService _sut;

    public LoteServiceTests()
    {
        _loteRepository = Substitute.For<ILoteRepository>();
        _currentUserProvider = Substitute.For<ICurrentUserProvider>();
        _currentUserProvider.TenantId.Returns(Guid.NewGuid());
        _sut = new LoteService(_loteRepository, _currentUserProvider);
    }

    [Fact]
    public async Task CriarLote_ComDadosValidos_DeveRetornarLoteDto()
    {
        // Arrange
        var request = new CriarLoteRequest
        {
            Nome = "Lote 001",
            QuantidadeAnimais = 30,
            PesoLiquidoEntrada = 15000
        };

        _loteRepository.AddAsync(Arg.Any<Lote>())
            .Returns(Task.CompletedTask);

        // Act
        var resultado = await _sut.CriarAsync(request);

        // Assert
        resultado.Should().NotBeNull();
        resultado.Nome.Should().Be("Lote 001");
        resultado.PesoMedioEntrada.Should().Be(500);
    }
}

Suites de Testes Unitarios

Suite Qtd Cenarios Cobertos
LoteServiceTests 8 CRUD, fechar lote, calculos KPI
AnimalServiceTests 5 CRUD, mudanca de status, validacoes
PesagemServiceTests 5 Registro, calculo GMD, historico
RacaoServiceTests 4 Compra, consumo, estoque
MedicamentoServiceTests 3 Aplicacao, custos, validacoes
OutrosGastosServiceTests 3 Registro, totalizacao, validacoes

Mocking com NSubstitute

// Configurar retorno do mock
_loteRepository.GetByIdAsync(loteId)
    .Returns(new Lote { Id = loteId, Nome = "Lote 001" });

// Verificar chamada ao mock
await _loteRepository.Received(1).UpdateAsync(Arg.Any<Lote>());

// Verificar argumento especifico
await _loteRepository.Received(1).UpdateAsync(
    Arg.Is<Lote>(l => l.Status == LoteStatus.Fechado));

Testes de Integracao (22 testes)

CustomWebApplicationFactory

Configura o ambiente de testes com banco de dados em memoria:

public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Substituir PostgreSQL por banco em memoria
            var descriptor = services.SingleOrDefault(
                d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
            services.Remove(descriptor);

            services.AddDbContext<AppDbContext>(options =>
            {
                options.UseInMemoryDatabase("TestDb");
            });

            // Seed de dados de teste
            var sp = services.BuildServiceProvider();
            using var scope = sp.CreateScope();
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            db.Database.EnsureCreated();
            SeedTestData(db);
        });
    }
}

AuthHelper

Utilitario para autenticar nos testes de integracao:

public static class AuthHelper
{
    public static async Task<string> GetTokenAsync(HttpClient client)
    {
        var response = await client.PostAsJsonAsync("/api/auth/login",
            new { Email = "admin@tepconfina.com", Password = "Admin@123" });

        var result = await response.Content
            .ReadFromJsonAsync<LoginResponse>();

        return result.Token;
    }

    public static void AddAuth(HttpClient client, string token)
    {
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", token);
    }
}

Exemplo de Teste de Integracao

public class LotesControllerTests : IClassFixture<CustomWebApplicationFactory>
{
    private readonly HttpClient _client;

    public LotesControllerTests(CustomWebApplicationFactory factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task GetLotes_Autenticado_DeveRetornar200()
    {
        // Arrange
        var token = await AuthHelper.GetTokenAsync(_client);
        AuthHelper.AddAuth(_client, token);

        // Act
        var response = await _client.GetAsync("/api/lotes");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);
        var lotes = await response.Content
            .ReadFromJsonAsync<List<LoteDto>>();
        lotes.Should().NotBeNull();
    }
}

Executando os Testes

Todos os testes

dotnet test TepConfina.sln

Apenas testes unitarios

dotnet test tests/TepConfina.UnitTests

Apenas testes de integracao

dotnet test tests/TepConfina.IntegrationTests

Com cobertura

dotnet test --collect:"XPlat Code Coverage"

Relatorio de cobertura

Use reportgenerator para gerar um relatorio HTML a partir do arquivo de cobertura.

Boas Praticas

  • Cada teste deve ser independente e isolado
  • Use nomes descritivos: Metodo_Cenario_ResultadoEsperado
  • Mock apenas dependencias externas (repositorios, servicos)
  • Testes de integracao usam banco em memoria para velocidade
  • Mantenha os testes rapidos (< 1s por teste unitario)