O que Testes React faz?
Padrões abrangentes de teste React para testes de componentes focados em comportamento, testes de hooks customizados, asserções de acessibilidade e mocking em nível de rede.
Quando Ativar
- Escrevendo testes para componentes React, hooks customizados ou páginas
- Adicionando cobertura de teste a componentes legados não testados
- Migrando de padrões Enzyme ou de era de classe para React Testing Library
- Configurando Vitest ou Jest para um novo projeto React
- Mockando 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 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 acionado, requisição enviada)
Um teste NÃO deve:
- Inspecionar estado do componente, props passadas para filhos ou quais hooks foram chamados
- Mockar 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 | Necessário motor de navegador real | Use quando o JSDOM não tiver 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 de função clara.
Prioridade de Consulta
O React Testing Library expõe consultas em três níveis — use de cima para baixo:
- Acessível para todos:
getByRole,getByLabelText,getByPlaceholderText,getByText,getByDisplayValue - Semântico:
getByAltText,getByTitle - IDs de teste (válvula de escape):
getByTestId
// Melhor
screen.getByRole("button", { name: /save/i });
// OK para inputs
screen.getByLabelText("Email");
// Último recurso
screen.getByTestId("save-btn");
Variantes:
getBy*— lança erro se não encontrarqueryBy*— retornanull(use para "afirmar ausência")findBy*— assíncrono, 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("submits the form", 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: /save/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("Loaded")).toBeInTheDocument();
// Afirmação de efeito colateral
await waitFor(() => expect(saveSpy).toHaveBeenCalled());
// Elemento que deve desaparecer
await waitForElementToBeRemoved(() => screen.queryByText("Loading"));
Nunca use setTimeout + asserção — é instável. Use os matchers acima.
Mocking de Rede com MSW
Mock Service Worker faz mocking na camada de rede. O componente, os hooks e a 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 mockada falhe o teste de forma ruidosa — aprovações silenciosas são piores que vermelhos.
Sobrescrita por teste
test("renders error on 500", async () => {
server.use(
http.get("/api/users/:id", () => new HttpResponse(null, { status: 500 })),
);
render(<UserPage id="1" />);
expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument();
});
Encapsulamento com 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 Customizados
import { renderHook, act } from "@testing-library/react";
test("useCounter increments and decrements", () => {
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 accepts initial value", () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
test("useUser fetches user data", async () => {
// Instancie o QueryClient UMA VEZ por teste fora do wrapper para que sobreviva a re-renderizações.
// Criá-lo dentro da closure do wrapper redefine o estado do cache em 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 que alteram estado em
act - Teste apenas por meio 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 has no a11y violations", async () => {
const { container } = render(<UserCard user={mockUser} />);
expect(await axe(container)).toHaveNoViolations();
});
Execute axe em testes de componente para cada componente interativo. Captura:
- Labels ausentes em inputs de formulário
- Uso inválido de ARIA
- Contraste de cor pobre (limitado — JSDOM não tem motor CSS real, portanto isso funciona apenas para estilos inline; contraste visual pertence ao Playwright)
- Texto alternativo ausente em imagens
- Violações de ordem de cabeçalho
Link cruzado: skills/accessibility/SKILL.md para o manual mais amplo de testes de acessibilidade.
Quando NÃO Usar Testes de Snapshot
Snapshots de saída renderizada:
- Quebram a cada mudança de estilo
- Recebem carimbo de borracha 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 puros (
formatInvoice(invoice)-> string estável) - Arquivos de configuração gerados (por exemplo, saída de configuração do webpack)
Para regressão visual em componentes, use screenshots do Playwright/Cypress ou Percy/Chromatic — diffs visuais reais, não strings de DOM.
Quando Usar Playwright / Cypress
JSDOM (usado pelo Vitest/Jest) não pode:
- Renderizar layout real (flexbox, grid, consultas de viewport)
- Executar animações nativas de 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 a DevTools
Para qualquer um desses, use Playwright Component Testing (teste de componente em navegador real) ou E2E completo. Veja e2e-testing skill.
Limite 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 customizados | >=85% |
| Componentes de apresentação | >=80% — comportamento, não linhas |
| Componentes container | >=70% — caminhos principais + 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,
},
},
}
Antipadrões
container.querySelector("...")— ignora consultas de acessibilidade, permite que testes passem quando usuários reais falhariam- Afirmar sobre número de renderizações — detalhe de implementação
jest.mock("react", ...)— nunca mocke React. Refatore o componente- Mockar componentes filhos por padrão — testa a integração, não o isolamento. Mock 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 — torna-se instável quando a ordem dos testes muda
- Testes que passam com
it.skip()removido — seu teste não afirma realmente o que você pensa
Fluxo de Trabalho TDD
RED -> Escreva teste falhando para o próximo requisito
GREEN -> Escreva código mínimo do componente para passar
REFACTOR -> Melhore o componente, os testes permanecem verdes
REPEAT -> 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 correto
- 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 # 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("submits user form and shows success", 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("Name"), "Alice");
await user.type(screen.getByLabelText("Email"), "[email protected]");
await user.click(screen.getByRole("button", { name: /save/i }));
expect(await screen.findByText(/saved successfully/i)).toBeInTheDocument();
});
Testando um error boundary
function Broken() {
throw new Error("boom");
}
test("error boundary renders fallback", () => {
// Suprime o ruído de console.error do React para o erro esperado e restaura em seguida para
// que o spy não vaze entre testes e esconda erros reais em outro lugar.
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
render(
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Broken />
</ErrorBoundary>,
);
expect(screen.getByText("Something went wrong")).toBeInTheDocument();
} finally {
errorSpy.mockRestore();
}
});
Testando um boundary Suspense
test("shows loading then content", async () => {
renderWithProviders(
<Suspense fallback={<div>Loading...</div>}>
<UserDetail id="1" />
</Suspense>,
);
expect(screen.getByText("Loading...")).toBeInTheDocument();
expect(await screen.findByText("Alice")).toBeInTheDocument();
});