Testes unitários no front-end: Porque às vezes seu pai foi comprar pão e não voltou
Era sexta à noite quando o Slack piscou. Bug em produção. A feature que “funcionou na minha máquina” tinha derrubado o fluxo de checkout de um cliente. Três horas depois, com olheiras e a tela brilhando no escuro, fiz a pergunta mais cara que um dev pode fazer: por que não escrevi aquele teste?
A resposta, na época, era a mesma que ouço até hoje em todo daily, code review e issue do GitHub: “front-end não precisa disso”. Uma crença tão enraizada quanto achar que o FGTS vai servir pra alguma coisa, e, no mesmo estilo, só parece inofensiva até o dia em que você realmente precisa. Aí simplesmente não está lá.
Essa história literalmente foi para boi dormir, mas vamos lá!
O que é um teste unitário, afinal
Pense como um contrato fechado em código. Você isola uma unidade (pode ser uma função, um componente, um hook) e assegura que ela faz exatamente o que prometeu.
function formatarMoeda(valor: number): string {
return new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(valor);
}
describe("formatarMoeda", () => {
it("formata valor positivo em reais", () => {
expect(formatarMoeda(1234.56)).toBe("R$ 1.234,56");
});
it("formata zero direito", () => {
expect(formatarMoeda(0)).toBe("R$ 0,00");
});
it("formata valor negativo com sinal", () => {
expect(formatarMoeda(-99.9)).toBe("-R$ 99,90");
});
});
Simples assim. Mas essa simplicidade tem consequências que demoram para aparecer, e quando aparecem, chegam caras.
Por que testes importam no front-end
Tem um argumento clássico que diz que teste unitário é coisa de back-end, onde a lógica é “séria”. Seria só HTML com estilo, o front-end. Fazia algum sentido em 2010, quando você jogava jQuery no CDN e chamava de solução.
Hoje é outra história. Validação de formulários complexos, cálculos financeiros, transformações de dados antes de mandar pro servidor, estado distribuído por dezenas de componentes e regras de exibição que mudam por perfil de usuário: tudo isso é lógica de negócio, ao menos para quem passou os últimos anos assistindo o front-end engolir responsabilidades que antes eram exclusivas do servidor.
Confiança pra refatorar. Me convenceu de vez. Sem cobertura, cada refatoração é um salto de fé: você muda o código, reza, abre o navegador e clica em tudo que consegue lembrar. Com testes, você refatora, roda npm test, vê 47 casos passando e sabe exatamente onde olhar se algum falhar.
Documentação que não apodrece. Comentários ficam velhos. Testes não podem, porque quando ficam o build quebra. Um describe('ao calcular o desconto progressivo') seguido de vários it('deve aplicar 10% para valores entre 100 e 500') conta a história do comportamento esperado com mais precisão do que qualquer README jamais contaria.
Regressão detectada antes da QA. Sabe a Andressa? Ela chega ao bug antes do cliente. (A Andressa real, que trabalhou comigo numa fintech em 2021, tinha um sexto sentido pra isso. Saía de uma sprint review sabendo exatamente o que iria quebrar na próxima.) Só que você podia ter chegado primeiro, na sua própria máquina, cinco segundos depois de salvar o arquivo. CI com checagens automáticas no GitHub Actions muda o custo de um bug de “reunião de post-mortem” pra “um warning que sumiu sozinho”.
Componente bem escrito já é testável. Tratar componentes como funções é a grande virada das ferramentas modernas: dado um input, produz um output previsível. Testabilidade embutida, sem esforço adicional.
function BotaoCarregando({
carregando,
label,
onClick,
}: {
carregando: boolean;
label: string;
onClick: () => void;
}) {
return (
<button disabled={carregando} onClick={onClick}>
{carregando ? 'Aguarde...' : label}
</button>
);
}
it('desabilita o botão durante carregamento', () => {
render(<BotaoCarregando carregando={true} label="Salvar" onClick={vi.fn()} />);
expect(screen.getByRole('button')).toBeDisabled();
});
Os argumentos contrários (e por que alguns fazem sentido)
Vou ser honesto. Quem diz que checagem unitária de front-end não vale a pena não está completamente sem razão, embora chegue à conclusão errada. E devo admitir que já concordei com boa parte desses argumentos, principalmente nos anos em que ficava refatorando testes toda semana porque o designer tinha mudado um botão.
“UI muda muito rápido” tem fundo de verdade. Escreve um teste verificando o texto exato de um botão, ou que o fundo do header é #2a2a2a, e você terá dor de cabeça cada vez que o designer atualizar o Figma. O que me irrita nesse argumento não é ele estar errado, é a conclusão que tiram dele. Repara: o problema está no que o teste cobre, não no fato de existir. Testar comportamento, não aparência. Que um formulário inválido não submite, não que o elemento de envio é vermelho.
“E2E já resolve” faz sentido em parte. Checagens end-to-end bem escritas pegam muito do que o unitário pegaria. Aqui devo ser justo: esse argumento tem mais força do que gosto de admitir. O problema é o preço: abrir um navegador, navegar pelo fluxo de cadastro e verificar o resultado demora segundos, enquanto checar a função de validação de CPF demora milissegundos. Com mil cenários, essa diferença dói muito. E quando o servidor de CI cai às 3 da manhã com o deploy parado, você prefere os rápidos.
“Custo de manutenção é alto” procede para coberturas mal construídas. Checagens frágeis, coladas nos detalhes internos do componente e que quebram por qualquer bobagem, são piores que nada: consomem tempo sem dar confiança real. A saída não é parar de escrever testes. Aprender a cobrir contratos, não o código interno, é o que muda o jogo (demorou uns três projetos pra internalizar isso, no meu caso). Quando adotei Testing Library em vez de Enzyme no meu time, esse problema simplesmente sumiu.
“Front-end é difícil de testar” era verdade. Em 2015, cobrir um componente React era aventura. Hoje você tem Testing Library, Vitest, Jest, jest-dom, MSW pra mock de API. O ambiente amadureceu bastante. Essa justificativa já passou do prazo de validade.
Onde traçar a linha
Ninguém tem tempo de cobrir tudo. Tudo bem. Melhor pergunta: o que merece atenção?
Regras de negócio puras: sempre. Formatação de dados, cálculos, validações, transformações, condicionais. Quanto mais complexa a lógica, mais a cobertura se paga, pelo que vi até aqui.
Peças de UI simples, aquelas que só renderizam um <p> com props, provavelmente não precisam de dedicação unitária específica, porque a integração já as pega de brinde.
Para componentes com comportamento, como formulários com validação, modais com estados e tabelas com ordenação, valem testes com Testing Library. Não é “unitário” no sentido puro, mas é próximo o suficiente e cobre o que importa.
Fluxos críticos de negócio, checkout, autenticação, preenchimento de dados sensíveis, merecem cobertura em todos os níveis: unitário para cada regra isolada, integração para o componente inteiro, E2E pro fluxo completo.
O custo real de não cobrir
Voltando àquela sexta-feira. Depois de corrigir o bug às duas da manhã, escrevi o teste que teria evitado tudo isso.
Demorou quatro minutos.
Aqui está o cálculo que ninguém faz na hora de “economizar tempo” pulando a cobertura. Pegar esse problema teria demorado quatro segundos num teste. No mundo real, custou horas de diagnóstico, deploy de emergência, conversa com cliente, estresse e, às vezes, dinheiro de verdade. Escrever testes parece gasto. Gasto não: é investimento com retorno invisível, que é talvez a pior combinação possível para convencer alguém a começar. Mas o retorno está lá, nos bugs que nunca chegaram até você.
Seu pai foi comprar pão e não voltou. Talvez se tivesse teste, tivesse voltado.
Escreva o teste.