Voltar para o Blog
Construindo em Público8 / 10
Construindo em PúblicoParte 8 de 10

Como testamos (e o que escolhemos não testar)

5 min de leitura
Compartilhar
Construindo em Público

Vou começar com uma confissão que costuma fazer gente torcer o nariz: o ClubPetro não tem uma suíte de testes unitários. Não é descuido, não é dívida técnica que a gente finge não ver — é uma escolha, feita de olhos abertos, sobre onde investir a energia limitada de um time pequeno. Este post é sobre essa escolha: o que a gente confia que não vai quebrar sem um teste explícito, o que a gente testa de verdade, e por que essa fronteira faz sentido pro nosso contexto. Spoiler: "não ter testes unitários" não é a mesma coisa que "não ter garantias".

O compilador é o nosso maior conjunto de testes

A maior parte do que uma suíte de testes unitários pegaria num time típico, aqui é pego pelo sistema de tipos. E não por acaso — a gente desenhou pra ser assim.

  • tRPC de ponta a ponta. O tipo do retorno de um procedure viaja até o componente que o consome. Renomeou um campo, esqueceu de atualizar uma tela? Não compila. Isso cobre a classe inteira de bugs de "a API mudou e o frontend não sabe" sem uma linha de teste.
  • Zod nas fronteiras. Todo input de procedure, todo webhook, toda env var passa por um schema. O que um teste de validação faria — garantir que lixo não entra no sistema — o Zod faz em runtime, na borda, de graça.
  • any proibido. Um any é um ponto cego que nenhum teste de tipo alcança. Bani-lo mantém a garantia do compilador intacta em todo o codebase.

Quando o tipo já prova que a forma dos dados está certa, um teste unitário que verifica a forma dos dados vira redundante. A gente prefere gastar a rigidez onde ela rende mais.

O que a gente testa de verdade: o comportamento, não a unidade

Não testar unidade não significa não verificar nada. Significa que a gente verifica no nível onde o bug realmente aparece pro usuário: o comportamento de ponta a ponta.

  • A gente dirige o fluxo, não a função. Antes de dar merge numa mudança não-trivial, o padrão é exercitar o fluxo afetado de verdade — abrir a tela, clicar, ver o dado certo aparecer — em vez de confiar só em typecheck. Um número certo no teste unitário e errado na tela é o pior dos mundos; a gente vai direto na tela.
  • Lógica de negócio vive isolada e pura. Em packages/domain, as funções são puras, sem I/O. Isso não é só bom pra testar — é bom pra ler. Você entende uma função de health score olhando pra ela, sem montar mock de banco. A pureza é a garantia; o teste seria só uma formalidade em cima dela.
  • Quality checks locais antes de tudo. lint, typecheck, build rodam local e de novo no CI. Um build que passa já provou muita coisa numa base tipada.

A pergunta que a gente faz não é "isso tem teste?". É "como eu sei que isso funciona?". Às vezes a resposta é o tipo. Às vezes é dirigir o fluxo. Raramente precisaria ser um teste unitário isolado.

O que a gente escolhe NÃO testar — e por quê

Aqui mora a parte honesta. Tem coisa que a gente conscientemente decide não cobrir, porque o custo do teste supera o risco que ele mitiga.

  • CRUD tipado. Um procedure que lê e devolve uma linha, com Zod na entrada e tipo no retorno, não ganha teste. O que quebraria já quebra no compilador.
  • Glue code de UI. Testar que um botão chama uma mutation é testar o framework, não a nossa lógica. A gente prefere olhar a tela.
  • Integrações externas voláteis. Sync de Google Ads, Evolution API de WhatsApp, RD Station — a superfície muda por fora do nosso controle. Um teste que "passa" contra um mock desatualizado dá falsa segurança. A gente investe em resiliência (isolar falha por item, try-catch por job, idempotência) em vez de em asserções.

A regra tácita: a gente não testa o que o tipo já garante, nem o que um mock só fingiria garantir. O esforço vai pra onde há risco real e não coberto — a lógica de domínio que faz conta com dinheiro e saúde de cliente, e os fluxos que o usuário toca.

Onde essa aposta cobra o preço

Nenhuma escolha é de graça, e seria desonesto fingir que essa é. O calcanhar de Aquiles é o que vive fora do alcance do compilador: os jobs do Trigger.dev e os crons de sync. Ali não tem tipo que salve você de uma lógica de negócio silenciosamente errada — um cron que processa a rede errada, um resumo que agrega o período errado. Foi exatamente onde a gente mais se queimou, e a resposta não foi "escreva testes unitários", foi "torne cada job resiliente e observável": isolar falha por item, logar o suficiente pra saber que quebrou antes do cliente. É uma troca deliberada, não um ponto cego que a gente ignora.

O que faríamos de novo

A aposta se sustenta: pra um time pequeno movendo rápido numa base fortemente tipada, o par tipos + verificação de comportamento entrega mais confiança por hora investida do que uma suíte unitária entregaria. A pureza do domínio e o contrato do tRPC fazem o trabalho pesado.

O que a gente ajustaria: ser ainda mais deliberado em blindar a fronteira não-tipada — os jobs e crons — já que é lá que a ausência de testes mais dói. Não com testes unitários, mas com resiliência e observabilidade.

Se você é o tipo de engenheiro que se irrita com dogma — que pergunta "esse teste me protege de quê, exatamente?" antes de escrevê-lo, e que sabe a diferença entre cobertura e confiança — você vai gostar de como a gente pensa aqui. A meta nunca foi ter testes. A meta sempre foi dormir tranquilo, e a gente escolhe o caminho mais curto pra isso em cada situação.

Curtiu como a gente pensa?

É assim que trabalhamos todo dia.

Se esse jeito de pensar é o seu, vem construir com a gente — dá uma olhada nas vagas abertas.

Ver vagas