Pruebas de componentes React
Prueba el renderizado de componentes, las interacciones del usuario, la UI condicional y el contenido dinámico con confianza.
Busca en todas las páginas de la documentación
Prueba el renderizado de componentes, las interacciones del usuario, la UI condicional y el contenido dinámico con confianza.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de referencia rápida - lista para copiar y pegar.
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();Cuándo usarlo: Siempre que construyas un componente que renderice props, maneje interacciones o muestre 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();
});
});
});Lo que demuestra esto:
queryBywithin()aria-label accesible para consultas dirigidas al botón de eliminarrender() crea un árbol DOM nuevo - las pruebas están aisladas por defectowithin() limita las consultas a un elemento contenedor específico, útil para probar elementos individuales de una listaqueryBy devuelve null en lugar de lanzar un error, lo que lo hace ideal para afirmar ausenciagetAllBy devuelve un array de elementos coincidentes - usa .toHaveLength() para afirmar el tamaño de listasPruebas con snapshot (úsalas con moderación):
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 salida pequeña:
it("renders correct class names", () => {
render(<Badge variant="success">OK</Badge>);
expect(screen.getByText("OK").className).toMatchInlineSnapshot(
`"badge badge-success"`
);
});Pruebas con rerender para cambios 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" })]} />);Probar detalles de implementación - Comprobar state interno, métodos de instancia o nombres de clase CSS ata las pruebas a la estructura del código. Solución: Afirma lo que ve el usuario - contenido de texto, visibilidad, estado habilitado/deshabilitado.
Depender demasiado de snapshots - Los snapshots grandes son difíciles de revisar y se aprueban sin mirar al actualizarse. Solución: Usa snapshots solo para salida pequeña y estable. Prefiere afirmaciones explícitas.
Olvidar esperar userEvent - Todos los métodos de userEvent devuelven promesas en v14+. Solución: Siempre haz await o la prueba puede pasar antes de que la interacción termine.
No usar within() para elementos de lista - screen.getByText("Eliminar") coincide con el primer botón de eliminar de la página. Solución: Limita el alcance al elemento de lista específico con within() o usa atributos aria-label únicos.
Probar demasiadas cosas en una sola prueba - Las pruebas que añaden, alternan y eliminan en un solo bloque it son difíciles de depurar cuando fallan. Solución: Escribe pruebas enfocadas para cada comportamiento.
| Alternativa | Úsala cuando | No la uses cuando |
|---|---|---|
| Storybook + Chromatic | Necesitas pruebas de regresión visual junto a las stories de componentes | Solo necesitas pruebas de comportamiento |
| Playwright Component Testing | Necesitas renderizado en un navegador real (canvas, CSS complejo) | Las pruebas unitarias rápidas de componentes son suficientes |
| Solo pruebas con snapshot | Proteger contra cambios no deseados en componentes pequeños y estables | Los componentes cambian con frecuencia o los snapshots son grandes |
Usa queryBy, que devuelve null en lugar de lanzar un error:
render(<Banner show={false} />);
expect(screen.queryByRole("banner")).not.toBeInTheDocument();Usa within() para limitar las consultas a un contenedor:
const items = screen.getAllByRole("listitem");
expect(within(items[0]).getByText("First")).toBeInTheDocument();toHaveTextContent o toBeInTheDocument.Usa rerender del resultado de render:
const { rerender } = render(<Alert severity="info" message="Note" />);
rerender(<Alert severity="error" message="Note" />);
expect(screen.getByRole("alert")).toHaveClass("alert-error");Si varios elementos contienen el mismo texto, getByText coincide con el primero. Usa within() para limitar el alcance a un padre específico, o usa atributos aria-label únicos.
let counter = 0;
vi.stubGlobal("crypto", {
randomUUID: () => `test-id-${++counter}`,
});En userEvent v14+, todos los métodos son asíncronos. Sin await, las interacciones pueden no completarse antes de que se ejecuten las afirmaciones, provocando falsos positivos o pruebas inestables.
function createTodo(overrides: Partial<Todo> = {}): Todo {
return {
id: crypto.randomUUID(),
text: "Default todo",
completed: false,
...overrides,
};
}Sí. La versión moderna de Testing Library limpia automáticamente después de cada prueba desmontando componentes y eliminando nodos del DOM. No necesitas llamar a cleanup() manualmente.
const items = screen.getAllByRole("listitem");
expect(items).toHaveLength(3);.snap separado.Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥