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
SglinxDbContextreal ou InMemory “para simplificar” mutação de estoque - Só verifica se
SaveChangesAsyncfoi chamado - Monta
WebApplicationFactoryouHttpClient
Esses cenários pertencem a testes de integração.
2. O que testar na SGApi (prioridade)
| Candidato | Exemplo | Projeto |
|---|---|---|
| Calculadoras / regras puras | Delta E/S do ajuste de estoque | AjusteEstoqueCalculator |
| Validadores de entrada | Quantidade negativa, IDs inválidos | Validators |
| Helpers | CnpjHelper, paginação | Infra, CompactMapperExtension |
| Mappers (sem EF) | DTO ↔ entidade quando lógica existe | *Mappings |
| Parsers | ArgsParser | Configuracao |
O que NÃO priorizar como unitário isolado
| Candidato | Motivo | Alternativa |
|---|---|---|
AjusteEstoqueService completo | Core é EF + transação | Integração |
| Controllers | Finos, delegam ao service | Integração |
RecebimentoMercadoriasService | Muito acoplado ao BD | Integraçã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
| Camada | Responsabilidade |
|---|---|
| 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