Fundamentos do React Testing Library
Teste seus componentes da maneira como os usuários interagem com eles -- consultando elementos acessíveis, não detalhes de implementação.
Busque em todas as páginas da documentação
Teste seus componentes da maneira como os usuários interagem com eles -- consultando elementos acessíveis, não detalhes de implementação.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida -- pronto para copiar e colar.
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
// 1. Renderize o componente
render(<LoginForm onSubmit={handleSubmit} />);
// 2. Consulte os elementos (por prioridade)
screen.getByRole("textbox", { name: /email/i }); // Melhor: role acessível
screen.getByLabelText(/password/i); // Bom: associação de label
screen.getByText(/submit/i); // OK: texto visível
screen.getByTestId("login-form"); // Último recurso: ID de teste
// 3. Interaja com userEvent (não fireEvent)
const user = userEvent.setup();
await user.type(screen.getByRole("textbox", { name: /email/i }), "alice@example.com");
await user.click(screen.getByRole("button", { name: /submit/i }));
// 4. Afirme os resultados
expect(screen.getByText(/welcome/i)).toBeInTheDocument();
// 5. Consultas assíncronas
const message = await screen.findByText(/success/i); // espera até 1s
await waitFor(() => expect(handleSubmit).toHaveBeenCalled());Quando usar isso: Toda vez que você escrever um teste de componente. Testing Library é o padrão para testar componentes React da perspectiva do usuário.
// src/components/login-form.tsx
"use client";
import { useState } from "react";
interface LoginFormProps {
onSubmit: (data: { email: string; password: string }) => Promise<void>;
}
export function LoginForm({ onSubmit }:LoginFormProps) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
setLoading(true);
try {
await onSubmit({ email, password });
} catch (err) {
setError(err instanceof Error ? err.message : "Login failed");
} finally {
setLoading(false);
}
}
return (
<form onSubmit={handleSubmit} aria-label="Login">
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
{error && <p role="alert">{error}</p>}
<button type="submit" disabled={loading}>
{loading ? "Logging in..." : "Log in"}
</button>
</form>
);
}// src/components/login-form.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi, describe, it, expect } from "vitest";
import { LoginForm } from "./login-form";
describe("LoginForm", () => {
const user = userEvent.setup();
it("submits email and password", async () => {
const handleSubmit = vi.fn().mockResolvedValue(undefined);
render(<LoginForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/email/i), "alice@example.com");
await user.type(screen.getByLabelText(/password/i), "password123");
await user.click(screen.getByRole("button", { name: /log in/i }));
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalledWith({
email: "alice@example.com",
password: "password123",
});
});
});
it("shows error on failed submission", async () => {
const handleSubmit = vi.fn().mockRejectedValue(new Error("Invalid credentials"));
render(<LoginForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/email/i), "alice@example.com");
await user.type(screen.getByLabelText(/password/i), "wrong");
await user.click(screen.getByRole("button", { name: /log in/i }));
expect(await screen.findByRole("alert")).toHaveTextContent("Invalid credentials");
});
it("disables button while loading", async () => {
const handleSubmit = vi.fn(() => new Promise(() => {})); // never resolves
render(<LoginForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/email/i), "alice@example.com");
await user.type(screen.getByLabelText(/password/i), "password123");
await user.click(screen.getByRole("button", { name: /log in/i }));
expect(screen.getByRole("button")).toBeDisabled();
expect(screen.getByRole("button")).toHaveTextContent("Logging in...");
});
});O que isso demonstra:
userEvent.setup() para simulação realista de digitação e cliqueswaitFor e findBy para asserções assíncronasrender() monta seu componente em um corpo de documento jsdom e retorna utilitários para limpeza (automática com o Testing Library moderno)screen é um objeto de conveniência que contém todas as consultas vinculadas a document.body -- não há necessidade de desestruturar de render()getBy (lança um erro se não encontrado), queryBy (retorna null se não encontrado), findBy (espera e tenta novamente)userEvent simula sequências completas de interação do usuário, incluindo eventos de foco, keydown, keypress, keyup, input e change -- fireEvent apenas dispara um único eventowaitFor tenta executar seu callback até que ele passe ou expire (padrão de 1000ms) -- use-o para asserções que dependem de atualizações de estado assíncronas| Prioridade | Consulta | Usar Quando |
|---|---|---|
| 1 | getByRole | O elemento tem uma role ARIA implícita ou explícita |
| 2 | getByLabelText | Campos de formulário com labels associados |
| 3 | getByPlaceholderText | Nenhum label disponível (prefira labels) |
| 4 | getByText | Elementos não interativos com texto visível |
| 5 | getByDisplayValue | Campos de formulário preenchidos |
| 6 | getByAltText | Imagens com texto alternativo |
| 7 | getByTitle | Elementos com atributo title |
| 8 | getByTestId | Último recurso quando nenhuma consulta semântica funciona |
| Variante | Sem Correspondência | 1 Correspondência | Múltiplas | Assíncrono |
|---|---|---|---|---|
getBy | lança erro | retorna | lança erro | não |
queryBy | null | retorna | lança erro | não |
findBy | lança erro | retorna | lança erro | sim |
getAllBy | lança erro | array | array | não |
queryAllBy | [] | array | array | não |
findAllBy | lança erro | array | array | sim |
// O resultado de render é tipado -- você pode acessar container e outros utilitários
const { container, rerender, unmount } = render(<MyComponent />);
// Render personalizado com wrapper para provedores
function renderWithProviders(ui: React.ReactElement) {
return render(ui, {
wrapper: ({ children }) => (
<ThemeProvider>{children}</ThemeProvider>
),
});
}Usar fireEvent em vez de userEvent -- fireEvent.change não simula digitação real (sem foco, sem teclas pressionadas). Correção: Sempre use userEvent.setup() e seus métodos (type, click, selectOptions).
Consultar por testId quando uma consulta por role funciona -- IDs de teste acoplam testes à implementação. Correção: Verifique a lista de roles ARIA -- a maioria dos elementos tem roles implícitas.
Envolver em act() manualmente -- O Testing Library moderno envolve render, userEvent e waitFor em act() para você. Correção: Remova chamadas manuais de act() a menos que você esteja disparando atualizações de estado fora das utilidades do Testing Library.
Não usar await nas chamadas de userEvent -- Os métodos userEvent são assíncronos a partir da v14+. Correção: Sempre use await em cada user.type(), user.click(), etc.
getBy para elementos que podem não existir -- getBy lança um erro se o elemento não for encontrado, o que interrompe o teste. Correção: Use queryBy ao afirmar que algo NÃO está no documento: expect(screen.queryByText("Error")).not.toBeInTheDocument().
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
| Enzyme | Testes de componentes de classe React legados (não recomendado para código novo) | Você usa React 18+ ou componentes de função |
| Playwright Component Testing | Você precisa de renderização real no navegador para testes visuais ou de layout | Você quer testes unitários rápidos no Node |
| Storybook interaction testing | Você já tem stories e quer testar interações dentro delas | Você precisa de um conjunto completo de testes com mocking e cobertura |
fireEvent.change dispara um único evento e ignora foco, keydown, keypress e keyup.userEvent simula a sequência completa de interação que um usuário real acionaria.userEvent.setup() e await seus métodos.getByRole -- role ARIA acessível (melhor)getByLabelText -- labels de campos de formuláriogetByPlaceholderText -- placeholders de inputgetByText -- conteúdo de texto visívelgetByTestId -- último recursogetBy lança um erro se o elemento não for encontrado (síncrono).queryBy retorna null se não for encontrado -- use para afirmar a ausência.findBy espera e tenta novamente até encontrar ou expirar o tempo limite (assíncrono).expect(screen.queryByText("Error")).not.toBeInTheDocument();Use queryBy (não getBy) porque getBy lança um erro quando o elemento está faltando.
O Testing Library moderno envolve render, userEvent e waitFor em act() automaticamente. Remova chamadas manuais de act() a menos que você acione atualizações de estado fora das utilidades do Testing Library.
Use waitFor ou findBy:
await waitFor(() => {
expect(handleSubmit).toHaveBeenCalled();
});
// ou
const message = await screen.findByText(/success/i);No userEvent v14+, todos os métodos são assíncronos. Sem await, o teste pode passar antes que a interação seja concluída, levando a falsos positivos ou falhas intermitentes.
function renderWithProviders(ui: React.ReactElement) {
return render(ui, {
wrapper: ({ children }) => (
<ThemeProvider>{children}</ThemeProvider>
),
});
}screen está vinculado a document.body e fornece todas as consultas sem a necessidade de desestruturar. Isso mantém o código de teste mais limpo e consistente.
const { container, rerender, unmount } = render(<MyComponent />);O tipo de retorno é inferido automaticamente. container é HTMLElement, rerender aceita ReactElement e unmount retorna void.
Ambos têm um padrão de 1000ms. Você pode personalizar o tempo limite:
await screen.findByText(/data/i, {}, { timeout: 3000 });Quando vários elementos compartilham a mesma role. A opção name corresponde ao nome acessível (texto do label, aria-label ou texto do botão):
screen.getByRole("button", { name: /submit/i });Revisado por Chris St. John·Última atualização: 7 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥