Pular para o conteúdo principal

Testes unitários — SGApi

Guia para escrever testes unitários de qualidade no projeto SGApi.


1. O que é (e o que não é) teste unitário

É unitário quando

  • Testa uma unidade de lógica (classe/método estático) sem I/O real
  • Não acessa banco, rede, disco ou HTTP
  • Dependências externas são mockadas ou fakes simples
  • Executa em milissegundos

Não é unitário quando

  • Usa SglinxDbContext real ou InMemory “para simplificar” mutação de estoque
  • Só verifica se SaveChangesAsync foi chamado
  • Monta WebApplicationFactory ou HttpClient

Esses cenários pertencem a testes de integração.


2. O que testar na SGApi (prioridade)

CandidatoExemploProjeto
Calculadoras / regras purasDelta E/S do ajuste de estoqueAjusteEstoqueCalculator
Validadores de entradaQuantidade negativa, IDs inválidosValidators
HelpersCnpjHelper, paginaçãoInfra, CompactMapperExtension
Mappers (sem EF)DTO ↔ entidade quando lógica existe*Mappings
ParsersArgsParserConfiguracao

O que NÃO priorizar como unitário isolado

CandidatoMotivoAlternativa
AjusteEstoqueService completoCore é EF + transaçãoIntegração
ControllersFinos, delegam ao serviceIntegração
RecebimentoMercadoriasServiceMuito acoplado ao BDIntegração + extrair regras

Estratégia: extrair regra testável quando o service crescer demais.


3. Estrutura de um teste unitário

Siga padrões e convenções.

namespace SGApi.UnitTests.Comercial.AjustesEstoque;

public class AjusteEstoqueCalculatorTests
{
[Theory]
[InlineData(1, 3, "E", 2, 2, 0)]
[InlineData(5, 2, "S", 3, 0, 3)]
[InlineData(4, 4, "", 0, 0, 0)]
public void Calcular_QuandoSaldoDiferenteDaQuantidadeAlvo_RetornaTipoEDeltaCorretos(
decimal saldoAtual,
decimal quantidadeAlvo,
string tipoEsperado,
decimal deltaEsperado,
decimal entradaEsperada,
decimal saidaEsperada)
{
// Arrange — dados já nos parâmetros

// Act
var resultado = AjusteEstoqueCalculator.Calcular(saldoAtual, quantidadeAlvo);

// Assert
resultado.Tipo.Should().Be(tipoEsperado);
resultado.Delta.Should().Be(deltaEsperado);
resultado.QuantidadeEntrada.Should().Be(entradaEsperada);
resultado.QuantidadeSaida.Should().Be(saidaEsperada);
}
}

4. Testar services com mocks (quando fizer sentido)

Use mocks apenas para colaboradores, nunca para o EF em fluxos de persistência.

Exemplo: validação antes de persistir

public class AjusteEstoqueServiceValidationTests
{
private readonly SglinxDbContext _context; // InMemory APENAS se testar query simples, não mutação
private readonly IParametroService _parametroService = Substitute.For<IParametroService>();

[Fact]
public async Task AjustarEstoque_QuandoProdutoInexistente_AdicionaErroESemPersistir()
{
// Arrange — context InMemory vazio OU mock de repositório abstraído
var sut = CriarService(...);

// Act
var result = await sut.AjustarEstoque(new AjusteEstoqueDto { ProdutoId = 999, ... });

// Assert
result.Errors.Should().ContainSingle(e => e.Contains("não encontrado"));
}
}

Atenção: teste unitário com InMemory não substitui integração para mutação. Use-o só para ramos de validação/early return.


5. NSubstitute — padrões SGApi

Retorno de parâmetro

_parametroService
.ConsultarValorParametro<int>("ATACAREJO_HABILITADO", 2)
.Returns(1);

Verificar interação (com moderação)

// ✅ OK — efeito colateral importante
_auditoriaService.Received(1).GravarAuditoria(
Arg.Any<int>(), "Ajuste Estoque", Arg.Any<string>(), "", 2);

// ❌ Evitar — acoplamento à implementação
_context.Received(1).SaveChangesAsync();

Async

_dateTimeProvider.DateTimeServer().Returns(DateTime.Parse("2026-06-01 10:00:00"));

6. Extrair lógica testável (refatoração recomendada)

Quando identificar regra no meio de um service:

Antes (difícil de testar):

if (produto.EstoqueAtual < quantidadeAjuste)
{
ajuste = quantidadeAjuste - produto.EstoqueAtual;
tipoOperacao = "E";
quantidadeEntrada = ajuste;
}

Depois:

// AjusteEstoqueCalculator.cs — classe estática ou serviço puro
public static AjusteEstoqueResult Calcular(decimal saldoAtual, decimal quantidadeAlvo);

Unitário cobre 100% dos ramos; integração confirma persistência.


7. Testes de mappers e DTOs

Teste quando houver transformação não trivial:

[Fact]
public void Map_MotivoAlteracaoEstoque_ParaDto_MapeiaCamposCorretos()
{
var entity = new MotivoAlteracaoEstoqueEntity { Id = 1, Descricao = "AJUSTE" };

var dto = entity.MapTo<MotivoAlteracaoEstoqueItemDto>();

dto.Id.Should().Be(1);
dto.Descricao.Should().Be("AJUSTE");
}

Não teste mappers 1:1 óbvios sem lógica — foco em valor.


8. Paginação e helpers

Consulte também docs/documentacao_paginacao.md. Exemplo unitário:

[Fact]
public void HasNextPage_QuandoTotalMaiorQuePageSize_RetornaTrue()
{
var paginacao = new PaginacaoDto { PageIndex = 0, PageSize = 10 };
var total = 11;

var hasNext = PaginacaoHelper.HasNextPage(paginacao, total);

hasNext.Should().BeTrue();
}

9. Organização de arquivos

SGApi.UnitTests/
├── Comercial/
│ └── AjustesEstoque/
│ ├── AjusteEstoqueCalculatorTests.cs
│ └── AjusteEstoqueAtacarejoTests.cs
├── Auth/
│ └── CnpjHelperTests.cs
├── Infra/
│ └── PaginacaoHelperTests.cs
└── Helpers/
└── TestDataFactory.cs

10. Checklist unitário SGApi

Antes de abrir PR:

  • Teste roda em < 100ms?
  • Sem banco, rede ou arquivo?
  • Nome descreve cenário e expectativa?
  • AAA respeitado?
  • [Theory] usado para variações da mesma regra?
  • Mocks só em colaboradores externos?
  • Se testa mutação de estoque → mover para integração

11. Relação com integração

CamadaResponsabilidade
Unitário“A regra de cálculo/decisão está correta?”
Integração“O dado foi persistido corretamente em todas as tabelas?”

Exemplo ajuste de estoque:

  • Unitário: delta 1→3 = tipo E, delta 2
  • Integração: es1.es2_qatu = 3, estoques, es2, es1g, mov002estoque0526

12. Referência rápida — pacotes

Ver setup-e-infraestrutura.md.