Voltar para o Blog
Quest Log

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

Testing e QA processo completo para desenvolvimento de jogos

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.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

Por onde começar amanhã

Não tente montar um processo de QA completo de uma vez, você vai abandonar. Comece pequeno:

  1. Escreva testes unitários pros sistemas críticos de lógica (vida, inventário, save). Esses pegam regressão de graça pra sempre.
  2. Crie um smoke test que verifica se a build sobe e chega no menu sem crashar. Rode em toda build.
  3. Anote todo bug num lugar só, com passos de reprodução. GitHub Issues serve.
  4. 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.