Testes e TDD no Frontend
Do Pyramid ao Trophy
Seção intitulada “Do Pyramid ao Trophy”A pirâmide de testes clássica prega uma base larga de testes unitários. No frontend, essa forma se distorce: unidades isoladas (um componente sem seus filhos, um reducer sem a store) dão pouca confiança porque o valor da UI está justamente na composição — componentes conversando entre si, com o roteador, com o estado e com a rede.
Kent C. Dodds propôs o Testing Trophy, cuja massa está na integração:
graph TD
E2E["E2E (Playwright/Cypress)<br/>poucos, caros, alta confiança"] --> INT
INT["Integration (RTL)<br/>MAIORIA — melhor custo/confiança"] --> UNIT
UNIT["Unit (Vitest/Jest)<br/>lógica pura, hooks, utils"] --> STATIC
STATIC["Static (TypeScript + ESLint)<br/>base gratuita"]
A régua permanece a mesma do Princípio FIRST: testes devem ser Fast, Isolated, Repeatable, Self-validating e Timely. O Trophy só reposiciona onde investir mais.
Testar comportamento, não implementação
Seção intitulada “Testar comportamento, não implementação”O princípio que orienta a React Testing Library:
“The more your tests resemble the way your software is used, the more confidence they can give you.”
Na prática: consulte o DOM como o usuário faria — por role, label, texto visível — e nunca por classe CSS, nome de componente ou estado interno. Um teste que quebra quando você renomeia um useState mas o usuário não percebe diferença é um teste acoplado à implementação. Isso conecta-se a Testabilidade e Arquitetura: código bem arquitetado é testável pela sua interface pública, não pelas suas vísceras.
Ferramentas e seus papéis
Seção intitulada “Ferramentas e seus papéis”- Vitest / Jest: runner e biblioteca de asserção. Vitest é o padrão em projetos Vite; Jest domina o legado. API praticamente intercambiável.
- React Testing Library (RTL): renderiza componentes e expõe queries centradas no usuário.
- MSW (Mock Service Worker): intercepta requisições na camada de rede. É um fake de servidor: em vez de mockar
fetch/axios(detalhe de implementação), você define handlers de request e responde como um backend real. Os testes não sabem como a rede é chamada. - Playwright / Cypress: E2E de verdade, subindo a aplicação num browser real. Poucos, focados nos fluxos críticos (login, checkout).
Exemplo 1 — Componente testado com RTL + MSW
Seção intitulada “Exemplo 1 — Componente testado com RTL + MSW”Componente que busca um usuário e o exibe. Note que o teste nunca inspeciona estado interno.
import { useEffect, useState } from "react";
type User = { id: string; name: string; email: string };
export function UserCard({ userId }: { userId: string }) { const [user, setUser] = useState<User | null>(null); const [error, setError] = useState(false);
useEffect(() => { let active = true; fetch(`/api/users/${userId}`) .then((r) => { if (!r.ok) throw new Error("failed"); return r.json(); }) .then((data: User) => active && setUser(data)) .catch(() => active && setError(true)); return () => { active = false; }; }, [userId]);
if (error) return <p role="alert">Não foi possível carregar o usuário.</p>; if (!user) return <p>Carregando…</p>;
return ( <article aria-label="Cartão do usuário"> <h2>{user.name}</h2> <a href={`mailto:${user.email}`}>{user.email}</a> </article> );}import { render, screen } from "@testing-library/react";import { setupServer } from "msw/node";import { http, HttpResponse } from "msw";import { UserCard } from "./UserCard";
const server = setupServer( http.get("/api/users/:id", ({ params }) => HttpResponse.json({ id: params.id, name: "Ada Lovelace", email: "ada@math.dev" }) ));
beforeAll(() => server.listen());afterEach(() => server.resetHandlers());afterAll(() => server.close());
test("exibe nome e email do usuário carregado", async () => { render(<UserCard userId="42" />);
expect(screen.getByText("Carregando…")).toBeInTheDocument();
expect(await screen.findByRole("heading", { name: "Ada Lovelace" })).toBeInTheDocument(); expect(screen.getByRole("link", { name: "ada@math.dev" })).toHaveAttribute( "href", "mailto:ada@math.dev" );});
test("mostra alerta quando a requisição falha", async () => { server.use( http.get("/api/users/:id", () => new HttpResponse(null, { status: 500 })) );
render(<UserCard userId="42" />);
expect(await screen.findByRole("alert")).toHaveTextContent( "Não foi possível carregar o usuário." );});Exemplo 2 — Custom hook testado isoladamente (unit)
Seção intitulada “Exemplo 2 — Custom hook testado isoladamente (unit)”Lógica pura de estado, sem UI, testada com renderHook. Este é o andar unit do Trophy (a base é a análise estática): rápido e determinístico.
import { useCallback, useState } from "react";
export function useCounter(initial = 0, step = 1) { const [count, setCount] = useState(initial); const increment = useCallback(() => setCount((c) => c + step), [step]); const decrement = useCallback(() => setCount((c) => c - step), [step]); const reset = useCallback(() => setCount(initial), [initial]); return { count, increment, decrement, reset };}import { renderHook, act } from "@testing-library/react";import { useCounter } from "./useCounter";
test("incrementa e decrementa respeitando o step", () => { const { result } = renderHook(() => useCounter(10, 5));
expect(result.current.count).toBe(10);
act(() => result.current.increment()); expect(result.current.count).toBe(15);
act(() => result.current.decrement()); expect(result.current.count).toBe(10);});
test("reset volta ao valor inicial", () => { const { result } = renderHook(() => useCounter(3));
act(() => { result.current.increment(); result.current.increment(); }); expect(result.current.count).toBe(5);
act(() => result.current.reset()); expect(result.current.count).toBe(3);});TDD no frontend
Seção intitulada “TDD no frontend”O ciclo red-green-refactor de Test-Driven Development funciona no front, mas com nuance: escrever um teste RTL antes de existir markup é natural quando você parte do comportamento (“o usuário deve ver X ao clicar em Y”). Para lógica pura (hooks, reducers, use cases), o TDD flui como no backend. Para o layout visual em si, TDD tende a atritar — regressão visual e revisão manual servem melhor ali.
Anti-padrões e erros comuns
Seção intitulada “Anti-padrões e erros comuns”- Testar detalhe de implementação. Buscar por classe CSS, inspecionar
stateinterno, usarwrapper.instance(). Quebra a cada refactor sem ganho de confiança. - Excesso de unitários mockando tudo. Mockar todos os filhos de um componente testa a fiação do mock, não o comportamento real. Prefira integração.
- Mockar
fetch/axiosdiretamente em vez de usar MSW. Amarra o teste ao cliente HTTP concreto; migrar deaxiosparafetchquebra dezenas de testes sem motivo. - E2E para tudo. Playwright é caro e lento; reserve-o para fluxos críticos. Violar isso mata o “F” (Fast) do Princípio FIRST.
act()warnings ignorados. Sinal de atualização de estado não aguardada; leva a flakiness — quebra o “R” (Repeatable).waitForcom asserção vazia ousetTimeoutfixo. Fonte clássica de teste intermitente.
Relações
Seção intitulada “Relações”- Área: Frontend
- Fundamento de testes: Testes e TDD, Test-Driven Development
- Taxonomia: Tipos de Teste - Unit, Integration e E2E
- Dublês: Test Doubles - Stub, Spy, Mock e Fake (MSW como fake de servidor)
- Qualidade: Princípio FIRST
- Habilitador: Testabilidade e Arquitetura e Arquitetura no Frontend (arquitetura boa torna o teste fácil)
- Rede: APIs - REST, GraphQL e gRPC
Perguntas de revisão
Seção intitulada “Perguntas de revisão”1. O que é o Testing Trophy e como difere da pirâmide clássica?
Resposta
É o modelo de Kent C. Dodds que concentra a maior parte do investimento em testes de integração (com base de tipagem estática do TypeScript, camada de unit para lógica pura e topo enxuto de E2E). Difere da pirâmide clássica, que prioriza uma base larga de unitários. No front, integração dá o melhor custo/confiança porque o valor da UI está na composição.
2. Por que RTL prega consultar o DOM por role/label/texto em vez de classe ou estado?
Resposta
Porque testes devem espelhar o uso real: o usuário enxerga papéis, rótulos e texto, não classes CSS ou useState. Consultar pela interface pública desacopla o teste da implementação — renomear uma variável interna ou trocar uma classe não deve quebrar o teste se o comportamento visível não mudou.
3. Que tipo de test double é o MSW e qual vantagem ele traz sobre mockar fetch?
Resposta
É um fake de servidor: intercepta na camada de rede e responde como um backend real via handlers. A vantagem sobre mockar fetch/axios é que o teste não depende do cliente HTTP concreto — trocar a biblioteca de rede não quebra os testes, e você exercita o código do jeito que ele roda em produção.
4. Como o ciclo de TDD se aplica (e onde atrita) no frontend?
Resposta
Flui bem para lógica pura (hooks, reducers, use cases) e para comportamento observável (“ao clicar, ver X”) escrito com RTL antes do markup. Atrita no layout visual puro, onde regressão visual e revisão manual funcionam melhor que asserções red-green-refactor.