Testando Componentes React
Teste a renderização de componentes, interações do usuário, UI condicional e conteúdo dinâmico com confiança.
Busque em todas as páginas da documentação
Teste a renderização de componentes, interações do usuário, UI condicional e conteúdo dinâmico com confiança.
🤖 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, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
// Test rendering with props
render(<Alert severity="warning" message="Disk full" />);
expect(screen.getByRole("alert")).toHaveTextContent("Disk full");
// Test user interactions
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: /delete/i }));
expect(screen.queryByText("Item 1")).not.toBeInTheDocument();
// Test conditional rendering
render(<Banner show={false} />);
expect(screen.queryByRole("banner")).not.toBeInTheDocument();
// Test lists
const items = screen.getAllByRole("listitem");
expect(items).toHaveLength(3);
expect(within(items[0]).getByText("First")).toBeInTheDocument();Quando usar isso: Sempre que você criar um componente que renderiza props, lida com interações ou mostra UI condicional.
// src/components/todo-list.tsx
"use client";
import { useState } from "react";
interface Todo {
id: string;
text: string;
completed: boolean;
}
export function TodoList({ initialTodos = [] }: { initialTodos?: Todo[] }) {
const [todos, setTodos] = useState<Todo[]>(initialTodos);
const [input, setInput] = useState("");
function addTodo() {
if (!input.trim()) return;
setTodos((prev) => [
...prev,
{ id: crypto.randomUUID(), text: input.trim(), completed: false },
]);
setInput("");
}
function toggleTodo(id: string) {
setTodos((prev) =>
prev.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
);
}
function deleteTodo(id: string) {
setTodos((prev) => prev.filter((t) => t.id !== id));
}
const remaining = todos.filter((t) => !t.completed).length;
return (
<section aria-label="Todo list">
<form
onSubmit={(e) => {
e.preventDefault();
addTodo();
}}
>
<label htmlFor="new-todo">New todo</label>
<input
id="new-todo"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button type="submit">Add</button>
</form>
{todos.length === 0 ? (
<p>No todos yet. Add one above!</p>
) : (
<>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span
style={{
textDecoration: todo.completed ? "line-through" : "none",
}}
>
{todo.text}
</span>
</label>
<button
onClick={() => deleteTodo(todo.id)}
aria-label={`Delete ${todo.text}`}
>
Delete
</button>
</li>
))}
</ul>
<p>{remaining} item{remaining !== 1 ? "s" : ""} remaining</p>
</>
)}
</section>
);
}// src/components/todo-list.test.tsx
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi, describe, it, expect, beforeEach } from "vitest";
import { TodoList } from "./todo-list";
// Mock crypto.randomUUID for deterministic IDs
beforeEach(() => {
let counter = 0;
vi.stubGlobal("crypto", {
randomUUID: () => `test-id-${++counter}`,
});
});
describe("TodoList", () => {
const user = userEvent.setup();
describe("empty state", () => {
it("shows empty message when no todos", () => {
render(<TodoList />);
expect(screen.getByText(/no todos yet/i)).toBeInTheDocument();
});
it("does not show the list", () => {
render(<TodoList />);
expect(screen.queryByRole("list")).not.toBeInTheDocument();
});
});
describe("rendering with initial todos", () => {
const todos = [
{ id: "1", text: "Write tests", completed: false },
{ id: "2", text: "Fix bugs", completed: true },
];
it("renders all todos", () => {
render(<TodoList initialTodos={todos} />);
expect(screen.getAllByRole("listitem")).toHaveLength(2);
expect(screen.getByText("Write tests")).toBeInTheDocument();
expect(screen.getByText("Fix bugs")).toBeInTheDocument();
});
it("shows completed todos with checked checkbox", () => {
render(<TodoList initialTodos={todos} />);
const items = screen.getAllByRole("listitem");
const fixBugsItem = items[1];
expect(within(fixBugsItem).getByRole("checkbox")).toBeChecked();
});
it("shows remaining count", () => {
render(<TodoList initialTodos={todos} />);
expect(screen.getByText("1 item remaining")).toBeInTheDocument();
});
});
describe("adding todos", () => {
it("adds a new todo on form submit", async () => {
render(<TodoList />);
await user.type(screen.getByLabelText(/new todo/i), "Buy milk");
await user.click(screen.getByRole("button", { name: /add/i }));
expect(screen.getByText("Buy milk")).toBeInTheDocument();
expect(screen.getByLabelText(/new todo/i)).toHaveValue("");
});
it("does not add empty todos", async () => {
render(<TodoList />);
await user.click(screen.getByRole("button", { name: /add/i }));
expect(screen.queryByRole("list")).not.toBeInTheDocument();
});
});
describe("toggling todos", () => {
it("toggles completion on checkbox click", async () => {
render(
<TodoList
initialTodos={[{ id: "1", text: "Test", completed: false }]}
/>
);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).not.toBeChecked();
await user.click(checkbox);
expect(checkbox).toBeChecked();
expect(screen.getByText("0 items remaining")).toBeInTheDocument();
});
});
describe("deleting todos", () => {
it("removes todo on delete click", async () => {
render(
<TodoList
initialTodos={[{ id: "1", text: "Test", completed: false }]}
/>
);
await user.click(screen.getByRole("button", { name: /delete test/i }));
expect(screen.queryByText("Test")).not.toBeInTheDocument();
expect(screen.getByText(/no todos yet/i)).toBeInTheDocument();
});
});
});O que isso demonstra:
queryBywithin()aria-label acessível para queries de botão de exclusão direcionadasrender() cria uma nova árvore DOM -- os testes são isolados por padrãowithin() escopa as queries para um elemento container específico, útil para testar itens de lista individuaisqueryBy retorna null em vez de lançar um erro, tornando-o ideal para afirmar a ausênciagetAllBy retorna um array de elementos correspondentes -- use .toHaveLength() para afirmar o tamanho da listaTeste de snapshot (use com moderação):
it("matches snapshot", () => {
const { container } = render(
<Alert severity="error" message="Something broke" />
);
expect(container.firstChild).toMatchSnapshot();
});
// Snapshots are useful for detecting unintended UI changes
// but are brittle and often auto-updated without reviewSnapshots inline para saídas pequenas:
it("renders correct class names", () => {
render(<Badge variant="success">OK</Badge>);
expect(screen.getByText("OK").className).toMatchInlineSnapshot(
`"badge badge-success"`
);
});Teste com rerender para alterações de props:
it("updates when severity changes", () => {
const { rerender } = render(<Alert severity="info" message="Note" />);
expect(screen.getByRole("alert")).toHaveClass("alert-info");
rerender(<Alert severity="error" message="Note" />);
expect(screen.getByRole("alert")).toHaveClass("alert-error");
});// Type-safe test factories for complex props
function createTodo(overrides: Partial<Todo> = {}): Todo {
return {
id: crypto.randomUUID(),
text: "Default todo",
completed: false,
...overrides,
};
}
// Use in tests
render(<TodoList initialTodos={[createTodo({ text: "Custom" })]} />);Testando detalhes de implementação -- Verificar estado interno, métodos de instância ou nomes de classes CSS vincula os testes à estrutura do código. Correção: Afirme o que o usuário vê -- conteúdo de texto, visibilidade, estado habilitado/desabilitado.
Dependência excessiva de snapshots -- Snapshots grandes são difíceis de revisar e são aprovados automaticamente na atualização. Correção: Use snapshots apenas para saídas pequenas e estáveis. Prefira asserções explícitas.
Esquecer de aguardar userEvent -- Todos os métodos userEvent retornam promessas na v14+. Correção: Sempre await eles ou o teste pode passar antes que a interação seja concluída.
Não usar within() para itens de lista -- screen.getByText("Excluir") corresponde ao primeiro botão de exclusão na página. Correção: Escopo para o item de lista específico com within() ou use atributos aria-label exclusivos.
Testando muitas coisas em um único teste -- Testes que adicionam, alternam e excluem em um único bloco it são difíceis de depurar quando falham. Correção: Escreva testes focados para cada comportamento.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| Storybook + Chromatic | Você precisa de testes de regressão visual junto com histórias de componentes | Você só precisa de testes comportamentais |
| Playwright Component Testing | Você precisa de renderização real em navegador (canvas, CSS complexo) | Testes de componente em nível de unidade rápidos são suficientes |
| Apenas teste de snapshot | Protegendo contra alterações não intencionais em componentes pequenos e estáveis | Componentes mudam com frequência ou snapshots são grandes |
Use queryBy, que retorna null em vez de lançar um erro:
render(<Banner show={false} />);
expect(screen.queryByRole("banner")).not.toBeInTheDocument();Use within() para escopar queries para um container:
const items = screen.getAllByRole("listitem");
expect(within(items[0]).getByText("First")).toBeInTheDocument();toHaveTextContent ou toBeInTheDocument.Use rerender do resultado do render:
const { rerender } = render(<Alert severity="info" message="Note" />);
rerender(<Alert severity="error" message="Note" />);
expect(screen.getByRole("alert")).toHaveClass("alert-error");Se vários elementos contiverem o mesmo texto, getByText corresponde ao primeiro. Use within() para escopar para um pai específico ou use atributos aria-label exclusivos.
let counter = 0;
vi.stubGlobal("crypto", {
randomUUID: () => `test-id-${++counter}`,
});No userEvent v14+, todos os métodos são assíncronos. Sem await, as interações podem não ser concluídas antes da execução das asserções, causando falsos positivos ou testes instáveis.
function createTodo(overrides: Partial<Todo> = {}): Todo {
return {
id: crypto.randomUUID(),
text: "Default todo",
completed: false,
...overrides,
};
}Sim. O Testing Library moderno limpa automaticamente após cada teste, desmontando componentes e removendo nós DOM. Você não precisa chamar cleanup() manualmente.
const items = screen.getAllByRole("listitem");
expect(items).toHaveLength(3);.snap separado.Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥