Pular para o conteúdo principal

Testes de integração — SGApi

Guia para escrever testes de integração funcionais — a principal linha de defesa do projeto contra regressões de persistência, transação e API.


1. Definição

Teste de integração na SGApi valida:

HTTP Request → Pipeline ASP.NET → Auth/JWT → Controller → Service → EF Core → MySQL → Response

Inclui, quando aplicável:

  • Tabelas dinâmicas (mov{empresa}estoque{MMYY}, log{empresa}venda{MMYY})
  • Dois DbContext na mesma transação (SglinxDbContext + SglinxDynamicDbContext)
  • Permissões [AuthorizePermission("...")]
  • Seed + reset de banco entre testes

2. Por que integração é prioridade na SGApi

O bug 043447 (ajuste de estoque) passou porque:

  • SaveChangesAsync() era chamado, mas entidades estavam detached (AsNoTracking)
  • Movimento gravava no contexto dinâmico; saldo não atualizava no _context
  • Teste manual olhou só a tela de movimentação

Teste unitário com mock do DbContext não teria detectado.
Teste de integração assertando es1.es2_qatu teria falhado.


3. Pré-requisitos

  • Docker instalado (Testcontainers)
  • Projetos configurados conforme setup-e-infraestrutura.md
  • public partial class Program { } no SGApi

4. Anatomia de um teste de integração

Classe base (recomendado)

[Collection("Integration")]
public abstract class IntegrationTestBase : IAsyncLifetime
{
protected readonly SgApiWebApplicationFactory Factory;
protected HttpClient Client = null!;
private DatabaseResetHelper _reset = new();

protected IntegrationTestBase(SgApiWebApplicationFactory factory)
{
Factory = factory;
}

public async Task InitializeAsync()
{
await _reset.InitializeAsync(Factory.ConnectionString);
await _reset.ResetAsync(Factory.ConnectionString);
Client = Factory.CreateAuthenticatedClient(permissoes: ["estoque"]);
}

public Task DisposeAsync() => Task.CompletedTask;
}

Teste completo — ajuste de estoque (referência)

public class AjustesEstoqueIntegrationTests : IntegrationTestBase
{
public AjustesEstoqueIntegrationTests(SgApiWebApplicationFactory factory) : base(factory) { }

[Fact]
public async Task PostAjustesEstoques_QuandoAjusteValido_AtualizaTodasAsTabelasDeNegocio()
{
// Arrange
await SeedProdutoComSaldo(produtoId: 33610, lojaId: 2, saldo: 1m);

var payload = new
{
id = 1,
produtoId = 33610,
lojaId = 2,
quantidade = 3m,
codigoBarras = "7896306625275",
motivoAlteracaoEstoqueId = 1,
usuarioId = 1
};

// Act
var response = await Client.PostAsJsonAsync("/produtos-gestao/ajustes-estoques", payload);

// Assert — HTTP
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<AjusteEstoqueResponseDto>();
body!.Errors.Should().BeEmpty();

// Assert — banco (CRÍTICO)
await using var scope = Factory.Services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<SglinxDbContext>();

var produto = await db.Produtos
.AsNoTracking()
.SingleAsync(p => p.ProdutoId == 33610 && p.EmpresaId == 2);
produto.EstoqueAtual.Should().Be(3m, "es1.es2_qatu");

var estoques = await db.Estoques
.AsNoTracking()
.SingleAsync(e => e.ProdutoId == 33610 && e.EmpresaId == 2);
estoques.EstoqueAtual.Should().Be(3m, "estoques.quantidade_atual");

var es2 = await db.ProdutoEstoque
.AsNoTracking()
.SingleAsync(e => e.ProdutoId == 33610 && e.EmpresaId == 2);
es2.EstoqueAtual.Should().Be(3m);
es2.QuantEntrada.Should().Be(2m);

var historico = await db.HistoricoAjuste
.AsNoTracking()
.Where(h => h.Codigo == 33610)
.OrderByDescending(h => h.Data)
.FirstAsync();
historico.Quantidade.Should().Be(2m);
historico.Tipo.Should().Be("E");

var dataServidor = await db.DateTimeServer();
var dynamicContext = scope.ServiceProvider
.GetRequiredService<SglinxDynamicContextFactory>()
.Create(2, dataServidor.Month, dataServidor.Year);

var movimentos = await dynamicContext.MovEstoque
.AsNoTracking()
.Where(m => m.ProdutoId == 33610 && m.EmpresaId == 2)
.ToListAsync();
movimentos.Should().ContainSingle();
movimentos[0].Modulo.Should().Be("ATUEST");
movimentos[0].EstoqueAnterior.Should().Be(1m);
}
}

5. Regra de assert para mutações

Para qualquer operação que altere dados, asserte:

PerguntaAssert
HTTP correto?Status + body
Saldo principal?es1.es2_qatu
Reserva/saldo auxiliar?estoques.quantidade_atual
Tabela auxiliar mensal?es2
Histórico?es1g, auditoria (se aplicável)
Movimento?mov*estoque* ou log*venda*
Transação atômica?Teste de falha (opcional): simular erro e verificar rollback

Template de checklist por rota

## Rota: POST /produtos-gestao/ajustes-estoques
- [ ] 200 happy path
- [ ] 400 produto inexistente
- [ ] 403 sem permissão estoque
- [ ] es1.es2_qatu atualizado
- [ ] estoques.quantidade_atual atualizado
- [ ] es2 atualizado/inserido
- [ ] es1g inserido
- [ ] mov{loja}estoque{MMYY} inserido
- [ ] idempotência / quantidade igual (sem alteração)

6. Cenários obrigatórios por rota

Mínimo 3 testes por endpoint de mutação:

CenárioObjetivo
Happy pathFluxo completo + asserts de BD
Erro de negócioProduto não existe, quantidade inválida, etc.
AutorizaçãoSem token / sem permissão → 401/403

Opcionais de alto valor:

  • Quantidade igual ao saldo → sem alteração no banco
  • Atacarejo habilitado → conversão por código de barras
  • Falha no meio → nada parcial commitado (transação)

7. Autenticação e permissões

Rotas SuperApp usam [Authorize] + [AuthorizePermission("...")].

Testar 403

[Fact]
public async Task PostAjustesEstoques_SemPermissaoEstoque_Retorna403()
{
var client = Factory.CreateAuthenticatedClient(permissoes: ["consultaprod"]); // sem "estoque"

var response = await Client.PostAsJsonAsync("/produtos-gestao/ajustes-estoques", payload);

response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
}

Sessão única SuperApp

Se SessaoTokenValidator estiver ativo, o seed deve incluir superapp_usuarios.sessao_id alinhado ao JWT, ou substituir o validador em ConfigureTestServices para testes.

Ver docs/sessao_unica_superapp.md.


8. Tabelas dinâmicas

SglinxDynamicContextFactory monta nomes como:

mov002estoque0526 → mov + empresa(3 dígitos) + estoque + mês + ano

No seed ou InitializeAsync:

  1. Garantir que a tabela do mês corrente existe (script ou serviço equivalente ao EstruturaMovEstoque do SG Linear).
  2. Fixar data via mock de DateTimeServer ou criar tabela para mês/ano conhecidos no seed.

9. Transações entre dois contextos

Rotas que usam _context + SglinxDynamicDbContext (ex.: ajuste de estoque) devem ter teste que valida atomicidade:

// Cenário avançado: forçar falha após salvar es1 e antes de mov
// Esperado: rollback de es1 também (quando transação compartilhada)

Documentar no teste por que todas as tabelas devem estar consistentes após sucesso.


10. Seed e isolamento

Seed

private async Task SeedProdutoComSaldo(long produtoId, int lojaId, decimal saldo)
{
await using var scope = Factory.Services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<SglinxDbContext>();

// Inserir empresa, es1, estoques, es2 se necessário
// Usar IDs fixos documentados no teste
await db.SaveChangesAsync();
}

Reset

  • Respawn antes de cada teste (preferido)
  • Ou transação com rollback (limitado se HTTP commita em outra conexão)

Nunca rodar integração contra BD de desenvolvimento compartilhado.


11. Rotas prioritárias (Fase 1)

MóduloRota / áreaTabelas críticas
Ajuste estoquePOST produtos-gestao/ajustes-estoqueses1, estoques, es2, es1g, mov*
TransferênciasPOST .../transferencias-internases1, mov*, transf_interna*
InventárioPOST .../inventarioses7*, es1
RecebimentoPOST .../recebimento-mercadoriasrecebimento*, es1
AuthPOST /seguranca/autenticacao/tokensuperapp_usuarios, JWT claims
Consulta produtoGET produtos-gestao/...es1, es1p (read-only)

12. Anti-padrões (integração)

❌ Não fazer✅ Fazer
Assert só status 200Assert HTTP + banco
Usar BD local do devTestcontainers MySQL
Dados “que já existiam”Seed explícito
Um teste para 10 rotasUm arquivo por módulo
Ignorar permissõesTestar 401/403
Paralelizar com mesmo container sem isolamentoCollection fixture + reset

13. Executar e debugar

# Todos os testes de integração
dotnet test SGApi.IntegrationTests --logger "console;verbosity=detailed"

# Filtrar classe
dotnet test --filter "FullyQualifiedName~AjustesEstoqueIntegrationTests"

# Um teste
dotnet test --filter "FullyQualifiedName~PostAjustesEstoques_QuandoAjusteValido"

Se falhar no CI e passar local: verificar Docker, timeout do container e cultura (decimal com vírgula — usar 3m nos asserts).


14. Checklist integração SGApi

  • Usa WebApplicationFactory + Testcontainers?
  • Reset de BD entre testes?
  • Client autenticado com permissão correta?
  • Happy path asserta todas as tabelas de negócio?
  • Cenário de erro de negócio?
  • Cenário 401/403?
  • Seed documentado (IDs fixos)?
  • Tabela dinâmica do mês criada no seed?
  • Teste falha se remover a correção de produção? (“teste pega o bug”)

15. Ver também