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
DbContextna 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:
| Pergunta | Assert |
|---|---|
| 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ário | Objetivo |
|---|---|
| Happy path | Fluxo completo + asserts de BD |
| Erro de negócio | Produto não existe, quantidade inválida, etc. |
| Autorização | Sem 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:
- Garantir que a tabela do mês corrente existe (script ou serviço equivalente ao
EstruturaMovEstoquedo SG Linear). - Fixar data via mock de
DateTimeServerou 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ódulo | Rota / área | Tabelas críticas |
|---|---|---|
| Ajuste estoque | POST produtos-gestao/ajustes-estoques | es1, estoques, es2, es1g, mov* |
| Transferências | POST .../transferencias-internas | es1, mov*, transf_interna* |
| Inventário | POST .../inventarios | es7*, es1 |
| Recebimento | POST .../recebimento-mercadorias | recebimento*, es1 |
| Auth | POST /seguranca/autenticacao/token | superapp_usuarios, JWT claims |
| Consulta produto | GET produtos-gestao/... | es1, es1p (read-only) |
12. Anti-padrões (integração)
| ❌ Não fazer | ✅ Fazer |
|---|---|
| Assert só status 200 | Assert HTTP + banco |
| Usar BD local do dev | Testcontainers MySQL |
| Dados “que já existiam” | Seed explícito |
| Um teste para 10 rotas | Um arquivo por módulo |
| Ignorar permissões | Testar 401/403 |
| Paralelizar com mesmo container sem isolamento | Collection 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
- Padrões e convenções
- Setup e infraestrutura
- Testes unitários — regras extraídas para calculadoras
- Testes E2E — smoke em staging