O que Testes React faz?
Padrões abrangentes de teste React para testes de componentes focados em comportamento, testes de hooks personalizados, asserções de acessibilidade e simulação em nível de rede.
Quando Ativar
- Escrevendo testes para componentes React, hooks personalizados ou páginas
- Adicionando cobertura de teste a componentes legados não testados
- Migrando de Enzyme ou padrões da era de classe para React Testing Library
- Configurando Vitest ou Jest para um novo projeto React
- Simulando requisições HTTP em testes
- Afirmando violações de acessibilidade
- Decidindo quais testes pertencem ao RTL vs Playwright Component Testing vs E2E completo
Princípio Central
Teste o que o usuário vê e faz, não os detalhes de implementação.
Um teste deve:
- Renderizar o componente com os mesmos provedores que ele tem em produção
- Interagir com ele por meio de consultas acessíveis (role, label) e
userEvent - Afirmar saída visível e efeitos colaterais observáveis (callback disparado, requisição enviada)
Um teste NÃO deve:
- Inspecionar estado do componente, props passadas para filhos ou quais hooks foram chamados
- Simular o próprio React ou hooks do framework
- Afirmar sobre o número de renderizações ou estrutura do DOM além do que afeta os usuários
Escolha da Biblioteca
| Executor | Quando | Nota |
|---|---|---|
| Vitest | Vite, Remix, configurações modernas | Mais rápido, ESM nativo, API compatível com Jest |
| Jest | Next.js, CRA, repositórios estabelecidos | Padrão para muitos projetos React |
| Playwright Component Testing | Motor de navegador real necessário | Use quando o JSDOM não tem o recurso necessário |
| Cypress Component Testing | Navegador real, Cypress já em uso | Alternativa ao Playwright CT |
Escolha um. Não execute RTL + Vitest E Playwright CT no mesmo repositório, a menos que você tenha uma separação clara de responsabilidades.
Prioridade de Consulta
O React Testing Library expõe consultas em três níveis — use de cima para baixo:
- Acessível a todos:
getByRole,getByLabelText,getByPlaceholderText,getByText,getByDisplayValue - Semântico:
getByAltText,getByTitle - IDs de Teste (escotilha de escape):
getByTestId
// Melhor
screen.getByRole("button", { name: /salvar/i });
// OK para inputs
screen.getByLabelText("Email");
// Último recurso
screen.getByTestId("save-btn");
Variantes:
getBy*— lança exceção se não houver correspondênciaqueryBy*— retornanull(use para "afirmar ausência")findBy*— assíncrona, retorna uma Promise (use para elementos que aparecem após trabalho assíncrono)
Interação do Usuário com userEvent
import userEvent from "@testing-library/user-event";
test("envia o formulário", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<UserForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText("Email"), "[email protected]");
await user.click(screen.getByRole("button", { name: /salvar/i }));
expect(onSubmit).toHaveBeenCalledWith({ email: "[email protected]" });
});
- Sempre use
awaitnas chamadas de userEvent - Chame
userEvent.setup()uma vez por teste, reutilize ouserretornado userEventsimula uma sequência real de navegador;fireEventdespacha um único evento sintético — prefirauserEvent
Padrões Assíncronos
// Elemento que aparece após trabalho assíncrono
expect(await screen.findByText("Carregado")).toBeInTheDocument();
// Afirmação de efeito colateral
await waitFor(() => expect(saveSpy).toHaveBeenCalled());
// Elemento que deve desaparecer
await waitForElementToBeRemoved(() => screen.queryByText("Carregando"));
Nunca use setTimeout + afirmação — é instável. Use os matchers acima.
Simulação de Rede com MSW
O Mock Service Worker simula na camada de rede. O componente, hooks e biblioteca de fetch se comportam exatamente como em produção.
Configuração
// test/setup.ts
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";
export const handlers = [
http.get("/api/users/:id", ({ params }) =>
HttpResponse.json({ id: params.id, name: "Alice" }),
),
http.post("/api/users", async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: "new-id", ...body }, { status: 201 });
}),
];
export const server = setupServer(...handlers);
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Configure onUnhandledRequest: "error" para que qualquer requisição não simulada falhe o teste de forma evidente — aprovações silenciosas são piores que falhas.
Sobrescrita por teste
test("renderiza erro em 500", async () => {
server.use(
http.get("/api/users/:id", () => new HttpResponse(null, { status: 500 })),
);
render(<UserPage id="1" />);
expect(await screen.findByText(/algo deu errado/i)).toBeInTheDocument();
});
Encapsulamento de Provedores
Encapsule os provedores uma vez em um test-utils.tsx:
// test-utils.tsx
import { render, RenderOptions } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
export function renderWithProviders(
ui: React.ReactElement,
options?: RenderOptions,
) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return render(
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={lightTheme}>
<MemoryRouter>{ui}</MemoryRouter>
</ThemeProvider>
</QueryClientProvider>,
options,
);
}
export * from "@testing-library/react";
Em seguida, use import { renderWithProviders, screen } from "test-utils" em cada arquivo de teste.
Teste de Hooks Personalizados
import { renderHook, act } from "@testing-library/react";
test("useCounter incrementa e decrementa", () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
act(() => result.current.increment());
expect(result.current.count).toBe(1);
act(() => result.current.decrement());
expect(result.current.count).toBe(0);
});
test("useCounter aceita valor inicial", () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
test("useUser busca dados do usuário", async () => {
// Instancie o QueryClient UMA VEZ por teste fora do wrapper para que ele sobreviva a re-renderizações.
// Criá-lo dentro do closure do wrapper redefine o cache a cada renderização, produzindo testes instáveis.
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
const { result } = renderHook(() => useUser("1"), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual({ id: "1", name: "Alice" });
});
- Encapsule chamadas de mudança de estado em
act - Teste apenas através da API pública do hook
- Para hooks que usam contexto, passe um
wrapper
Asserções de Acessibilidade
import { axe, toHaveNoViolations } from "jest-axe"; // ou vitest-axe
expect.extend(toHaveNoViolations);
test("UserCard não tem violações de a11y", async () => {
const { container } = render(<UserCard user={mockUser} />);
expect(await axe(container)).toHaveNoViolations();
});
Execute axe em testes de componente para cada componente interativo. Detecta:
- Rótulos ausentes em inputs de formulário
- Uso inválido de ARIA
- Contraste de cor pobre (limitado — JSDOM não tem motor CSS real, então isso funciona apenas para estilos inline; contraste visual pertence ao Playwright)
- Texto alternativo ausente em imagens
- Violações de ordem de cabeçalhos
Link cruzado: skills/accessibility/SKILL.md para o guia mais amplo de testes de a11y.
Quando NÃO Usar Testes de Snapshot
Snapshots da saída renderizada:
- Quebram a cada mudança de estilo
- São aprovados mecanicamente durante a revisão
- Testam detalhe de implementação (estrutura do DOM), não comportamento
Usos aceitáveis de snapshot:
- Funções de serialização de dados pura (
formatInvoice(invoice)-> string estável) - Arquivos de configuração gerados (ex.: saída de configuração do webpack)
Para regressão visual em componentes, use screenshots do Playwright/Cypress ou Percy/Chromatic — diferenças visuais reais, não strings do DOM.
Quando Usar Playwright / Cypress
O JSDOM (usado pelo Vitest/Jest) não pode:
- Renderizar layout real (flexbox, grid, consultas de viewport)
- Executar animações nativas do navegador, transições CSS
- Testar comportamento de rolagem, arrastar e soltar, colar da área de transferência
- Lidar com iframes, popups, downloads, fluxos de origem cruzada
- Executar rede real em um ambiente controlado com suporte completo do DevTools
Para qualquer um desses casos, use Playwright Component Testing (teste de componente em navegador real) ou E2E completo. Veja e2e-testing skill.
Fronteira de decisão:
- Um hook, um componente de apresentação, um formulário com lógica -> RTL
- Um componente cujo layout importa ou que usa APIs de navegador não presentes no JSDOM -> Playwright CT
- Um fluxo de usuário completo em várias páginas -> Playwright/Cypress E2E
Metas de Cobertura
| Camada | Meta |
|---|---|
| Utilitários puros | >=90% |
| Hooks personalizados | >=85% |
| Componentes de apresentação | >=80% — comportamento, não linhas |
| Componentes contêiner | >=70% — caminhos felizes + estados de erro |
| Páginas | E2E coberto separadamente; mínimo de teste de fumaça |
Configure via vitest.config.ts / jest.config.js:
// vitest.config.ts
test: {
coverage: {
provider: "v8",
reporter: ["text", "html", "lcov"],
thresholds: {
lines: 80,
functions: 80,
branches: 70,
statements: 80,
},
},
}
Anti-Padrões
container.querySelector("...")— ignora consultas de acessibilidade, permite que testes passem quando usuários reais falhariam- Afirmar sobre o número de renderizações — detalhe de implementação
jest.mock("react", ...)— nunca simule o React. Refatore o componente- Simular componentes filhos por padrão — testa a integração, não o isolamento. Simule apenas quando o filho tem efeitos colaterais pesados
- Ignorar avisos de
act()— eles sinalizam bugs reais (atualização de estado após desmontagem, encapsulamento assíncrono ausente) - Compartilhar estado mutável entre testes — instabilidade quando a ordem dos testes muda
- Testes que passam com
it.skip()removido — seu teste não afirma realmente o que você pensa
Workflow TDD
VERMELHO -> Escreva um teste falhando para o próximo requisito
VERDE -> Escreva o código mínimo do componente para passar
REFATORAR -> Melhore o componente, os testes permanecem verdes
REPETIR -> Próximo requisito
Para novos componentes:
- Defina o tipo de prop e a assinatura do componente
- Escreva o primeiro teste para o caso mais simples
- Verifique se ele falha pelo motivo certo
- Implemente o suficiente para passar
- Adicione o próximo caso de teste
- Refatore quando o terceiro teste semelhante revelar um padrão
Comandos de Teste
# Vitest
vitest # modo watch
vitest run # execução única
vitest run --coverage # com cobertura
vitest run path/to/file.test.tsx # arquivo único
# Jest
jest --watch
jest --coverage
jest path/to/file.test.tsx
# Modo CI
CI=true vitest run --coverage
Relacionados
- Regras: rules/react/testing.md
- Skills: react-patterns, accessibility, e2e-testing, tdd-workflow
- Agentes:
react-reviewer(revisa a qualidade dos testes durante a revisão de código),tdd-guide(aplica o processo TDD) - Comandos:
/react-test,/react-review
Exemplos
Envio de formulário com MSW e userEvent
test("envia formulário de usuário e mostra sucesso", async () => {
server.use(
http.post("/api/users", () =>
HttpResponse.json({ id: "1", name: "Alice" }, { status: 201 }),
),
);
const user = userEvent.setup();
renderWithProviders(<UserForm />);
await user.type(screen.getByLabelText("Nome"), "Alice");
await user.type(screen.getByLabelText("Email"), "[email protected]");
await user.click(screen.getByRole("button", { name: /salvar/i }));
expect(await screen.findByText(/salvo com sucesso/i)).toBeInTheDocument();
});
Testando um boundary de erro
function Broken() {
throw new Error("boom");
}
test("boundary de erro renderiza fallback", () => {
// Suprime o ruído de console.error do React para o erro esperado, depois restaura para
// que o espião não vaze entre testes e esconda erros reais em outros lugares.
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
render(
<ErrorBoundary fallback={<div>Algo deu errado</div>}>
<Broken />
</ErrorBoundary>,
);
expect(screen.getByText("Algo deu errado")).toBeInTheDocument();
} finally {
errorSpy.mockRestore();
}
});
Testando um boundary Suspense
test("mostra carregando e depois conteúdo", async () => {
renderWithProviders(
<Suspense fallback={<div>Carregando...</div>}>
<UserDetail id="1" />
</Suspense>,
);
expect(screen.getByText("Carregando...")).toBeInTheDocument();
expect(await screen.findByText("Alice")).toBeInTheDocument();
});