Testing e QA em Jogos: Processo Completo de Garantia de Qualidade

Guia definitivo de testing e QA para jogos: metodologias, automação, ferramentas e estratégias para lançar jogos sem bugs
Testing e QA em Jogos: Processo Completo de Garantia de Qualidade
Eu já lancei jogo com bug que só apareceu na máquina do jogador, nunca na minha. E aprendi do jeito mais caro: QA não é a fase chata no fim do projeto, é o que separa um lançamento que você tem orgulho de um que você passa três semanas apagando incêndio no Discord.
Esse guia é sobre como testar jogo de verdade. Tipos de teste, o que dá pra automatizar e o que não dá, como organizar bug tracking sem virar burocracia, e o básico de certificação de console e mobile. Sem fórmula mágica, é processo mesmo.
Por que bug custa caro
Um bug que trava o jogo (crash, save corrompido, soft lock) não é só um review negativo. É refund, é nota baixa na loja que afunda seu alcance, e é a sua reputação como dev. O detalhe que mais machuca: o custo de um bug cresce conforme ele avança no projeto.
Achar um bug na sua própria máquina enquanto você programa a feature custa minutos. O mesmo bug achado depois do lançamento custa um patch de emergência, certificação de novo (no console), e a confiança de quem já comprou. Por isso QA não é "testar no final". É testar cedo, contínuo, e em camadas.
Os tipos de teste que importam
Não existe "testar o jogo" como tarefa única. Existem categorias diferentes, cada uma pegando um tipo de problema. Vale conhecer todas porque a maioria dos devs solo testa só uma (funcional) e ignora o resto.
- Funcional: a feature faz o que deveria? Menu navega, save/load funciona, progressão não trava. É o teste mais óbvio e o mais fácil de automatizar.
- Compatibilidade: roda em GPU diferente, resolução diferente, controle diferente, SO diferente? Aqui mora a maioria dos bugs que "só acontecem na máquina do jogador".
- Performance: FPS estável, tempo de loading aceitável, uso de memória que não estoura, sem thermal throttling. Dá pra medir e automatizar bem.
- Gameplay: dificuldade calibrada, progressão satisfatória, mecânica intuitiva. Isso é teste humano. Não tem como automatizar "é divertido?".
- Localização: tradução correta, texto que não estoura a caixa, formato de data e moeda certo pra cada região.
- Compliance: rating (ESRB/PEGI), requisitos técnicos da plataforma, padrões de acessibilidade. No console é obrigatório pra passar na certificação.
- Multiplayer: matchmaking, sincronização, compensação de lag, prevenção de trapaça. O mais difícil de testar porque depende de condições de rede que você não controla.
- Segurança: exploits de memória, manipulação de save, proteção de dados. Mais relevante quanto mais o jogo lida com economia online ou dados do jogador.
A real é que você não tem time pra cobrir tudo com a mesma profundidade. Então priorize: funcional e gameplay são críticos sempre. O resto você escala conforme o escopo (multiplayer só importa se tem multiplayer, compliance só pesa se vai pra console).
Metodologias: black box, white box e o resto
Antes de sair clicando, vale entender o vocabulário, porque ele define quem testa o quê.
- Black box: testa sem olhar o código. É o ponto de vista do jogador. Você sabe o que deveria acontecer e verifica se acontece, sem saber como o sistema funciona por dentro. É o que um tester contratado ou um beta tester faz.
- White box: testa com acesso ao código. É o que você faz quando escreve teste unitário pra uma função específica, sabendo exatamente os caminhos que ela pode tomar.
- Gray box: meio termo. Conhece a estrutura geral mas testa pela interface.
- Exploratório: sem script. O tester joga livre tentando quebrar coisa. Acha os bugs mais criativos, mas é difícil de reproduzir depois.
- Regressão: re-testa o que já funcionava depois de uma mudança. Existe pra pegar o clássico "consertei A e quebrei B".
- Smoke test: teste rápido pra ver se a build não está completamente quebrada antes de testar a sério. "Abre? Chega no menu? Inicia uma partida?" Se falha aqui, nem adianta continuar.
O fluxo prático na maioria dos estúdios: smoke test em cada build nova, teste funcional dos sistemas tocados, exploratório pra caçar o inesperado, e regressão antes de cada marco. Você não escolhe uma metodologia, você combina elas conforme o momento do projeto.
Anatomia de um caso de teste
Um caso de teste bom não é "testar o pulo". É específico o suficiente pra outra pessoa reproduzir sem te perguntar nada:
ID: TC-042
Título: Pulo perde altura ao segurar o botão por mais de 1s
Pré-condição: Personagem no chão, em terreno plano
Passos:
1. Segurar o botão de pulo por 2 segundos
2. Soltar
Resultado esperado: Altura máxima do pulo é atingida e mantida estável
Resultado obtido: Personagem sobe e começa a descer com botão ainda pressionado
Status: Falhou
Prioridade: Alta (afeta movimentação, mecânica central)
O segredo está nos passos numerados e no resultado esperado escrito antes de testar. Sem isso, "está bugado" vira uma discussão infinita sobre o que é o comportamento correto.
Automação: o que vale e o que não vale automatizar
Automação de teste em jogo é diferente de software comum. Boa parte do jogo é visual, temporal e subjetiva, e isso não cabe num assert. A regra que eu sigo: automatize a lógica, teste o feeling na mão.
O que vale automatizar:
- Lógica de sistemas (dano, inventário, economia, progressão)
- Cálculos de balanceamento (uma arma que deveria matar em 3 tiros mata em 3 tiros?)
- Performance (path finding abaixo de X ms, memória que não vaza)
- Smoke tests (a build sobe, carrega a cena principal, não crasha em 30s)
O que não vale (ou não compensa) automatizar:
- "É divertido?" Impossível de medir com código.
- Game feel, responsividade percebida, polish. Precisa de olho humano.
- Bugs visuais sutis (clipping, z-fighting, animação engraçada). Existe teste de imagem, mas é frágil e dá muito falso positivo.
Teste unitário de verdade em Unity
Aqui é onde automação brilha. O Unity Test Framework usa NUnit e roda direto no editor. Você testa a lógica do jogo isolada, sem precisar rodar o jogo inteiro. Esse exemplo testa um sistema de vida do jogador, daqueles que todo jogo tem:
using NUnit.Framework;
using UnityEngine;
public class PlayerHealthTests
{
private GameObject playerObject;
private PlayerController player;
[SetUp]
public void Setup()
{
playerObject = new GameObject("Player");
player = playerObject.AddComponent<PlayerController>();
player.Initialize(maxHealth: 100, damage: 10);
}
[Test]
public void TakeDamage_ReduzVida()
{
player.TakeDamage(20f);
Assert.AreEqual(80f, player.Health);
}
[Test]
public void DanoLetal_MataOJogador()
{
player.TakeDamage(player.Health + 10f);
Assert.IsTrue(player.IsDead);
Assert.AreEqual(0f, player.Health, "Vida não deve ficar negativa");
}
[Test]
public void Cura_NaoPassaDoMaximo()
{
player.TakeDamage(50f);
player.Heal(999f);
Assert.AreEqual(player.MaxHealth, player.Health);
}
[TearDown]
public void TearDown()
{
Object.DestroyImmediate(playerObject);
}
}
Repare no padrão: cada teste verifica uma coisa e o nome diz exatamente o quê. Cura_NaoPassaDoMaximo é o tipo de teste que pega o bug clássico de curar além do limite, que ninguém lembra de checar manualmente. O [SetUp] cria um jogador novo antes de cada teste e o [TearDown] destrói depois, então um teste nunca contamina o outro.
Pra testar coisa que depende de tempo ou física (como um pulo, que precisa de frames passando), o Unity tem [UnityTest] com corrotina:
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
public class PlayerJumpTests
{
[UnityTest]
public IEnumerator Pulo_AumentaPosicaoY()
{
var playerObject = new GameObject("Player");
playerObject.AddComponent<Rigidbody>();
var player = playerObject.AddComponent<PlayerController>();
float yInicial = player.transform.position.y;
player.Jump();
// espera meio segundo de física rodar
yield return new WaitForSeconds(0.5f);
Assert.Greater(player.transform.position.y, yInicial);
Object.DestroyImmediate(playerObject);
}
}
A diferença é o yield return: ele deixa o motor avançar frames de verdade antes do assert. Teste comum ([Test]) roda num frame só, então não serve pra nada que envolva física ou tempo.
O mesmo em Godot (GDScript)
Se você usa Godot, o equivalente é o GUT (Godot Unit Test). A ideia é idêntica, só muda a sintaxe:
extends GutTest
var player
func before_each():
player = Player.new()
player.initialize(100, 10) # vida_maxima, dano
add_child_autofree(player)
func test_dano_reduz_vida():
player.take_damage(20)
assert_eq(player.health, 80)
func test_dano_letal_mata():
player.take_damage(player.health + 10)
assert_true(player.is_dead)
assert_eq(player.health, 0, "Vida não deve ficar negativa")
func test_cura_nao_passa_do_maximo():
player.take_damage(50)
player.heal(999)
assert_eq(player.health, player.max_health)
Mesmo princípio: before_each prepara, cada teste verifica uma coisa, add_child_autofree limpa o nó depois. Engine diferente, disciplina igual.
Testes parametrizados: vários casos, um teste
Quando você quer testar a mesma lógica com valores diferentes, não copie e cole o teste. Parametrize. Isso aqui testa remoção de item do inventário com várias combinações de quantidade:
[TestCase(5, 3, 2)] // tinha 5, remove 3, sobra 2
[TestCase(10, 10, 0)] // remove tudo
[TestCase(1, 2, 0)] // tenta remover mais do que tem
public void RemoverItem_AtualizaQuantidade(int inicial, int remover, int esperado)
{
var inventory = new Inventory(slots: 10);
inventory.AddItem(new Item { Id = "flecha", Stackable = true }, inicial);
inventory.RemoveItem("flecha", remover);
Assert.AreEqual(esperado, inventory.GetItemQuantity("flecha"));
}
O caso (1, 2, 0) é o importante: testa o que acontece quando você tenta remover mais do que tem. Esse é o tipo de borda que vira bug de quantidade negativa no inventário se ninguém testar.
Performance: medir, não chutar
"Tá rodando liso aqui" não é teste de performance. Performance precisa de número. No Unity, o pacote Performance Testing deixa você medir um método e travar uma asserção de tempo:
using NUnit.Framework;
using Unity.PerformanceTesting;
public class PathfindingPerformanceTests
{
[Test, Performance]
public void Pathfinding_AbaixoDe5ms()
{
Measure.Method(() =>
{
var pathfinder = new AStarPathfinder();
pathfinder.FindPath(new Vector3(0, 0, 0), new Vector3(100, 0, 100));
})
.WarmupCount(10)
.MeasurementCount(100)
.Run();
}
}
O WarmupCount roda algumas vezes antes de medir (pra não contar o custo de aquecimento do JIT e dos caches), e o MeasurementCount mede 100 vezes pra você olhar a mediana, não um único número que pode ter sido sorte ou azar. Resultado aparece no Performance Test Report do editor.
Pra vazamento de memória, o teste é mais grosseiro mas funciona: force a coleta de lixo, faça a operação suspeita mil vezes, force de novo, e compare. Se a memória cresceu muito, algo não está sendo liberado:
[Test]
public void PoolDeInimigos_NaoVazaMemoria()
{
long memoriaInicial = GC.GetTotalMemory(forceFullCollection: true);
var pool = new ObjectPool<Enemy>(100);
for (int i = 0; i < 1000; i++)
{
var enemy = pool.Get();
enemy.Initialize();
pool.Return(enemy);
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
long crescimento = GC.GetTotalMemory(forceFullCollection: true) - memoriaInicial;
Assert.Less(crescimento, 1024 * 1024, "Memória cresceu mais de 1MB com object pool");
}
Object pool existe justamente pra não ficar criando e destruindo objeto a torto e a direito (que gera lixo e trava o jogo no GC). Esse teste garante que o pool está fazendo o trabalho dele: reusar, não acumular.
Bug tracking sem virar burocracia
Bug que você não anota é bug que você vai re-descobrir daqui a duas semanas. Mas também não precisa de um sistema enterprise pra rastrear bug de projeto solo. Precisa de consistência.
Um bug report útil tem o mínimo:
- Título que descreve o sintoma, não o palpite ("Save some ao trocar de fase", não "problema no SaveManager")
- Passos de reprodução numerados. Se você não consegue reproduzir, o bug é quase impossível de consertar.
- Esperado vs obtido
- Severidade: crítico (trava/corrompe), maior (feature quebrada), menor (cosmético)
- Ambiente: plataforma, build, specs da máquina
A distinção que mais economiza tempo é severidade vs prioridade. Severidade é o quão grave o bug é tecnicamente. Prioridade é o quão rápido você precisa consertar. Um crash num menu escondido que ninguém acessa é severidade alta, prioridade baixa. Um texto cortado na tela inicial é severidade baixa, prioridade alta (todo mundo vê). Não trate as duas como a mesma coisa.
Pra organizar isso, qualquer ferramenta serve. Pra time pequeno ou solo, GitHub Issues já resolve e ainda fica colado no código. Pra time maior, Jira é o padrão da indústria, e tem alternativas mais leves como Trello ou Linear. O que mata o tracking não é a ferramenta, é a inconsistência: meio time anotando, meio time consertando "na memória".
Detecção de duplicata
Conforme a lista de bugs cresce, você vai receber o mesmo bug reportado de três jeitos diferentes. Um truque simples que dá pra implementar sem nada de IA: comparar a similaridade dos títulos com a biblioteca padrão do Python e avisar quando dois passam de um limite.
from difflib import SequenceMatcher
def possivel_duplicata(titulo_novo, bugs_abertos, limite=0.8):
titulo_novo = titulo_novo.lower()
for bug in bugs_abertos:
similaridade = SequenceMatcher(None, titulo_novo, bug.titulo.lower()).ratio()
if similaridade > limite:
return bug
return None
SequenceMatcher vem na difflib, sem instalar nada. Não é mágica, é comparação de strings: "Save corrompe ao trocar de fase" e "save some na transição de fase" não vão bater por similaridade de título, então isso não substitui um humano olhando. Mas pega os duplicados óbvios e já reduz ruído.
Testes de plataforma e certificação
Aqui o jogo muda. PC você publica e pronto. Console (PlayStation, Xbox, Nintendo) tem certificação obrigatória: um conjunto de requisitos técnicos que a fabricante exige antes de deixar seu jogo na loja. Reprovar significa atraso de semanas e custo de retestar.
Cada plataforma tem o nome dela pra isso (TRC na PlayStation, XR na Xbox, Lotcheck na Nintendo), mas os temas se repetem:
- Estabilidade: o jogo não pode crashar em sessões longas. O teste típico é horas de gameplay contínuo sem travar.
- Save data: interromper o save (tirar energia, fechar o app no meio) não pode corromper o arquivo. Tem que falhar de forma graciosa, com mensagem, não com save quebrado.
- Suspend/resume e sleep: o console pode ser suspenso a qualquer momento. Seu jogo tem que voltar exatamente de onde parou.
- Terminologia correta: usar os nomes oficiais dos botões e recursos da plataforma. Parece bobo, reprova certificação.
- Transições de estado (Switch): docked pra handheld e vice-versa, troca de Joy-Con, troca de usuário. Tudo isso no meio do jogo, sem travar.
A lição prática: leia os requisitos de certificação no começo do projeto, não no fim. Muito desses requisitos (como tratar interrupção de save) mudam a arquitetura do seu sistema. Descobrir isso na reta final do lançamento é o que transforma certificação em pesadelo.
Testes em mobile
Mobile é o oposto do console em uma coisa: não tem hardware fixo. São milhares de combinações de aparelho, versão de SO, tamanho de tela e quantidade de RAM. Você não testa em todos, testa numa amostra representativa.
A estratégia que funciona é escolher aparelhos por faixa, não por modelo:
- Topo de linha (8GB+ de RAM): seu teto de qualidade gráfica
- Intermediário (4GB): onde está a maioria dos jogadores
- Entrada (2-3GB de RAM): seu piso, o aparelho que define o mínimo que o jogo precisa rodar
Se roda bem no aparelho de entrada, roda em todos acima. Por isso o aparelho fraco é o mais importante de ter na mesa.
Os testes que mais pegam problema em mobile:
- FPS no aparelho mais fraco. Defina um alvo (30 FPS estável é o piso aceitável pra maioria) e meça nele, não no seu celular caro.
- Interrupções: ligação no meio do jogo, notificação, troca de app. O jogo tem que pausar e voltar sem quebrar. É o bug mobile mais comum e o mais ignorado.
- Bateria e aquecimento: jogo que esquenta o aparelho e drena bateria rápido perde retenção, mesmo sendo bom.
- Notch e tela com recorte: garantir que botão ou informação importante não fica embaixo do recorte da câmera.
- Orientação e tamanho de tela: a UI escala direito de um celular pequeno pra um tablet?
Pra rodar em aparelho real sem comprar uma gaveta de celulares, existem serviços de cloud como Firebase Test Lab (Android) e farms de dispositivo. Útil pra cobrir a cauda longa de aparelhos que você não tem fisicamente.
Planejando os testes ao longo do projeto
QA não é uma fase, é um processo que acompanha o desenvolvimento. Os marcos clássicos de produção te dão a estrutura:
- Alpha: o jogo está jogável de ponta a ponta mas incompleto. Foco em teste funcional e gameplay. É aqui que você descobre se a ideia funciona.
- Beta: features completas, conteúdo entrando. Foco amplia pra performance, compatibilidade e (se tiver) multiplayer. Beta fechado com jogadores reais aparece aqui.
- Release Candidate: nada de feature nova, só caça de bug e regressão. A pergunta é "está pronto pra lançar?".
- Gold/Master: a build final. Foco no caminho crítico e em compliance. Daqui sai a versão que vai pra loja (e o plano do patch de day one, porque sempre tem alguma coisa).
O critério que importa em cada marco é o exit criteria: o que precisa ser verdade pra avançar. Por exemplo, pra sair de Release Candidate: zero bug crítico, performance no alvo, certificação aprovada. Sem critério escrito, "está pronto" vira opinião, e a opinião sempre cede pra pressão de prazo.
Um truque de planejamento: monte o cronograma de trás pra frente. Pegue a data de lançamento e vá subtraindo o tempo de cada fase (certificação, RC, beta, alpha). Isso te mostra a data real em que cada fase precisava ter começado, e geralmente revela que você tem menos tempo de QA do que imaginava.
Ferramentas que valem conhecer
Sem propaganda, só o que a maioria dos times usa:
- Testes automatizados: Unity Test Framework (Unity), GUT (Godot), Unreal Automation (Unreal)
- Bug tracking: GitHub Issues (solo/time pequeno), Jira (padrão da indústria), Linear ou Trello (leves)
- Performance: Unity Profiler, RenderDoc (debug de GPU), Unreal Insights
- Mobile/dispositivo real: Firebase Test Lab (Android), TestFlight (beta iOS)
Comece pelo que já vem com a sua engine. Profiler e Test Framework do Unity (ou Godot) cobrem 80% do que um dev solo precisa, e são grátis.
Por onde começar amanhã
Não tente montar um processo de QA completo de uma vez, você vai abandonar. Comece pequeno:
- Escreva testes unitários pros sistemas críticos de lógica (vida, inventário, save). Esses pegam regressão de graça pra sempre.
- Crie um smoke test que verifica se a build sobe e chega no menu sem crashar. Rode em toda build.
- Anote todo bug num lugar só, com passos de reprodução. GitHub Issues serve.
- Teste no aparelho mais fraco que você tem acesso, não no melhor.
A regra que resume tudo: é sempre mais barato achar o bug agora do que depois do lançamento. Quanto mais cedo no projeto o teste acontece, menos ele custa. QA não é gasto, é seguro contra o lançamento que você não quer ter.


