Guia de testes de integração
Atualizado em: 2026-09-05
Os testes de integração exercitam a aplicação headless por HTTP, com persistência, conteúdo, regras e serialização reais. Eles devem se comportar como um pequeno cliente Godot: enviar intenções, observar versões e confiar somente nas respostas da engine.
Onde ficam
tests/API.Tests/
├── Controllers/ contratos HTTP e erros
├── Integration/ fluxos completos de jogo
├── Helpers/ cliente de teste
└── TestWebApplicationFactory.cs
TestWebApplicationFactory inicializa uma API isolada. Cada teste deve criar sua
própria run e não depender da ordem de execução de outros testes.
Executar
dotnet test tests/API.Tests/API.Tests.csproj
Para investigar um cenário:
dotnet test tests/API.Tests/API.Tests.csproj --filter "FullyQualifiedName~DeterministicPrototypeFlowTests"
Rode também o Core quando a mudança atravessar regras de domínio:
dotnet test tests/Core.Tests/Core.Tests.csproj
dotnet test tests/API.Tests/API.Tests.csproj
Cliente de teste
GameEngineClientSimulator encapsula o envelope determinístico e reduz boilerplate.
Ele não é uma API alternativa: internamente usa apenas as rotas v1 canônicas.
Operações centrais disponíveis:
Task<Guid> StartRunAsync(
string configName = "default",
string runDefinitionId = "default_run",
string playerEntityId = "player",
ulong? seed = null);
Task<JsonElement> GetRunStateAsync(Guid runId);
Task<List<string>> DrawCardsAsync(Guid runId, int count = 1);
Task<JsonElement> DiscardCardsAsync(Guid runId, IEnumerable<string> cardIds);
Task<List<string>> GetHandAsync(Guid runId);
Task<Guid> StartCombatAsync(
string playerActorId,
IEnumerable<string> otherActorIds,
IReadOnlyDictionary<string, float>? initialPlayerResourceValues = null,
Guid? runId = null,
string heroDefinitionId = "player_warrior",
string enemyDefinitionId = "enemy_goblin");
Task<JsonElement> GetCombatStateAsync(Guid combatId);
Task<JsonElement> ExecuteActionAsync(
Guid combatId,
string actorId,
string? targetId = null,
string? powerId = null,
string? cardId = null,
Guid? runId = null);
Task<JsonElement> EndTurnAsync(Guid combatId, Guid? runId = null);
O parâmetro de recursos iniciais é genérico; os nomes do helper de teste não definem papéis no contrato da engine:
var overrides = new Dictionary<string, float>
{
["energy"] = 3,
["stability"] = 8
};
var combatId = await Client.StartCombatAsync(
"player",
new[] { "enemy_1" },
overrides,
runId);
Não adicione novamente parâmetros como initialEnergy. O helper deve refletir o
modelo de recursos da API.
Estrutura recomendada de um teste
public sealed class ExampleFlowTests : GameEngineIntegrationTestBase
{
public ExampleFlowTests(TestWebApplicationFactory factory)
: base(factory)
{
}
[Fact]
public async Task FixedSeed_RunCanBeVerified()
{
var runId = await Client.StartRunAsync(seed: 424242);
var state = await Client.GetRunStateAsync(runId);
Assert.Equal((ulong)424242, state.GetProperty("seed").GetUInt64());
using var response = await RawClient.PostAsync(
$"/api/v1/runs/{runId}/verify",
content: null);
response.EnsureSuccessStatusCode();
var verification = await response.Content.ReadFromJsonAsync<JsonElement>();
Assert.True(verification.GetProperty("isValid").GetBoolean());
}
}
Use Arrange, Act e Assert, mas prefira um fluxo completo pequeno a mocks internos. Quando a regra é puramente local, cubra-a no Core e deixe no teste de integração apenas a prova de que HTTP, conteúdo e persistência estão conectados corretamente.
Cenário mínimo de combate
- Crie a run com seed fixa.
- Compre cartas pelo comando de run, se o modo exigir.
- Inicie
START_ENCOUNTERcom aliases e definições explícitas. - Leia a mão e obtenha o
cardInstanceId. - Consulte a avaliação da carta.
- Envie
PLAY_CARDcom sequência e step observados. - Confirme recursos, zonas de carta e fila de resolução.
- Leia o journal e execute a verificação semântica.
- Repita o cenário do zero e compare hashes.
O teste não deve calcular o dano esperado com uma implementação paralela. Ele pode afirmar um valor conhecido do fixture e deve comparar a execução com a prévia e o journal produzidos pela própria engine.
Testando recursos genéricos
Todo comportamento que pareça específico de vida ou energia deve ter ao menos um caso com um ID arbitrário.
Casos mínimos:
- materializar
stabilitya partir de JSON; - sobrescrever seu valor no início do encontro;
- consumi-lo como custo;
- reduzi-lo com
DAMAGE; - restaurá-lo com
HEAL; - usá-lo como influência de um pipeline;
- alterar mínimo ou máximo;
- disparar
DefeatOwnerpor política; - provar que a categoria, sozinha, não derrota;
- falhar a segunda mutação de um lote e confirmar rollback total.
Testando cartas
Use instâncias reais da mão. Uma definição como basic_attack não substitui um
cardInstanceId no comando de gameplay.
Valide:
- avaliação em lote e individual;
- custo escolhido e affordability;
- alvos válidos e inválidos;
- definição efetiva após upgrade;
- influências de status, relíquia e recurso;
- igualdade entre prévia e aplicação;
- transição de zona após sucesso;
- preservação da mão e dos recursos após falha.
Concorrência e idempotência
Um teste de comando deve cobrir três situações:
- request novo com versões atuais: aceito;
- retry byte-equivalente com o mesmo
commandId: mesmo recibo, sem efeito duplo; - comando novo com versões antigas:
409, sem alteração.
Não gere um novo commandId ao simular retry. Isso representa uma nova intenção e
não testa idempotência.
Timeline e branches
Para o sandbox:
- execute comandos suficientes para formar mais de um ponto;
- recupere o estado histórico por sequência;
- crie duas branches do mesmo ponto;
- aplique decisões diferentes;
- confirme que a origem permaneceu inalterada;
- verifique replay e hashes de cada branch.
Eventos
Eventos são validados depois do commit. O teste deve primeiro confirmar o estado persistido e depois consultar polling ou SSE. Nunca use a chegada do evento como prova única de que a transação ocorreu.
Teste paginação/cursor, filtro por run ou combate e retomada sem duplicação. Para recuperação do cliente, descarte a projeção local e reconstrua a tela pelo read model antes de continuar.
Teste de conteúdo revisionado
- Crie uma run na revisão A.
- Publique uma revisão B com uma regra numericamente diferente.
- Confirme que a run A mantém o resultado antigo.
- Crie uma run B e confirme a regra nova.
- Verifique semanticamente ambas.
Se o modo permitir troca de revisão em runtime, a ativação deve ser um comando explícito, persistido e testado; editar arquivo nunca basta para mudar uma run.
Diagnóstico de falhas
409 VersionConflict
O teste reutilizou sequência ou step antigos. Releia a run e o combate antes de criar a próxima intenção.
422 RuleViolation
Inspecione ProblemDetails, avaliação da carta, comandos disponíveis, ator ativo e
fase. Não altere o cliente para contornar a regra.
404
Confirme se o ID é da run/combate atual e se o fixture publica a definição na mesma revisão.
resultado não determinístico
Compare seed, revisão, ordem dos comandos, payload canônico, sequence/step e fingerprints dos cálculos. Evite relógio real, GUID aleatório de domínio e iteração não ordenada dentro da engine.
Boas práticas
- Use uma seed explícita em cenários reproduzíveis.
- Mantenha fixtures mínimos e legíveis.
- Não dependa de arquivos gerados por uma execução manual da API.
- Não use rotas removidas ou helpers que escondam uma mutação fora do gateway.
- Afirme atomicidade em todos os caminhos de falha relevantes.
- Compare hashes em testes de replay, não apenas campos escolhidos.
- Dê ao teste um nome que descreva regra e resultado.
- Preserve o corpo HTTP ao reportar uma falha de contrato.