Testando Contexto e Gerenciamento de Estado
Teste componentes que dependem de React Context, stores Zustand ou outros provedores de estado globais.
Busque em todas as páginas da documentação
Teste componentes que dependem de React Context, stores Zustand ou outros provedores de estado globais.
🤖 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 } from "@testing-library/react";
// Render customizado com providers
function renderWithProviders(
ui: React.ReactElement,
{ theme = "light" }: { theme?: string } = {}
) {
return render(ui, {
wrapper: ({ children }) => (
<ThemeProvider initialTheme={theme}>
<AuthProvider>{children}</AuthProvider>
</ThemeProvider>
),
});
}
// Use em testes
renderWithProviders(<Dashboard />, { theme: "dark" });
// Reinicie o store Zustand entre os testes
import { useCartStore } from "@/stores/cart";
beforeEach(() => {
useCartStore.setState({ items: [], total: 0 });
});Quando usar isso: Quando componentes leem de React Context ou de um store Zustand e você precisa controlar os valores fornecidos nos testes.
// src/context/theme-context.tsx
"use client";
import { createContext, useContext, useState, useCallback } from "react";
type Theme = "light" | "dark";
interface ThemeContextValue {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
export function ThemeProvider({
children,
initialTheme = "light",
}: {
children: React.ReactNode;
initialTheme?: Theme;
}) {
const [theme, setTheme] = useState<Theme>(initialTheme);
const toggleTheme = useCallback(
() => setTheme((t) => (t === "light" ? "dark" : "light")),
[]
);
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
return ctx;
}// src/components/theme-toggle.tsx
"use client";
import { useTheme } from "@/context/theme-context";
export function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme} aria-label="Toggle theme">
{theme === "light" ? "Switch to dark" : "Switch to light"}
</button>
);
}// src/components/theme-toggle.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect } from "vitest";
import { ThemeProvider } from "@/context/theme-context";
import { ThemeToggle } from "./theme-toggle";
function renderWithTheme(ui: React.ReactElement, initialTheme: "light" | "dark" = "light") {
return render(ui, {
wrapper: ({ children }) => (
<ThemeProvider initialTheme={initialTheme}>{children}</ThemeProvider>
),
});
}
describe("ThemeToggle", () => {
const user = userEvent.setup();
it("shows current theme", () => {
renderWithTheme(<ThemeToggle />);
expect(
screen.getByRole("button", { name: /toggle theme/i })
).toHaveTextContent("Switch to dark");
});
it("starts in dark mode when configured", () => {
renderWithTheme(<ThemeToggle />, "dark");
expect(screen.getByRole("button")).toHaveTextContent("Switch to light");
});
it("toggles theme on click", async () => {
renderWithTheme(<ThemeToggle />);
const button = screen.getByRole("button", { name: /toggle theme/i });
expect(button).toHaveTextContent("Switch to dark");
await user.click(button);
expect(button).toHaveTextContent("Switch to light");
await user.click(button);
expect(button).toHaveTextContent("Switch to dark");
});
it("throws when used outside provider", () => {
// Suppress console.error for expected error
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
expect(() => render(<ThemeToggle />)).toThrow(
"useTheme must be used within ThemeProvider"
);
spy.mockRestore();
});
});// src/stores/cart.ts
import { create } from "zustand";
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface CartStore {
items: CartItem[];
addItem: (item: Omit<CartItem, "quantity">) => void;
removeItem: (id: string) => void;
updateQuantity: (id: string, quantity: number) => void;
total: () => number;
clearCart: () => void;
}
export const useCartStore = create<CartStore>((set, get) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id);
if (existing) {
return {
items: state.items.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
};
}
return { items: [...state.items, { ...item, quantity: 1 }] };
}),
removeItem: (id) =>
set((state) => ({
items: state.items.filter((i) => i.id !== id),
})),
updateQuantity: (id, quantity) =>
set((state) => ({
items: state.items.map((i) =>
i.id === id ? { ...i, quantity: Math.max(0, quantity) } : i
),
})),
total: () =>
get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
clearCart: () => set({ items: [] }),
}));// src/stores/cart.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { useCartStore } from "./cart";
describe("CartStore", () => {
beforeEach(() => {
// Reset store to initial state between tests
useCartStore.setState({ items: [] });
});
it("adds an item", () => {
useCartStore.getState().addItem({ id: "1", name: "Widget", price: 10 });
const { items } = useCartStore.getState();
expect(items).toHaveLength(1);
expect(items[0]).toEqual({
id: "1",
name: "Widget",
price: 10,
quantity: 1,
});
});
it("increments quantity for duplicate items", () => {
const { addItem } = useCartStore.getState();
addItem({ id: "1", name: "Widget", price: 10 });
addItem({ id: "1", name: "Widget", price: 10 });
const { items } = useCartStore.getState();
expect(items).toHaveLength(1);
expect(items[0].quantity).toBe(2);
});
it("removes an item", () => {
useCartStore.setState({
items: [{ id: "1", name: "Widget", price: 10, quantity: 1 }],
});
useCartStore.getState().removeItem("1");
expect(useCartStore.getState().items).toHaveLength(0);
});
it("updates quantity", () => {
useCartStore.setState({
items: [{ id: "1", name: "Widget", price: 10, quantity: 1 }],
});
useCartStore.getState().updateQuantity("1", 5);
expect(useCartStore.getState().items[0].quantity).toBe(5);
});
it("calculates total", () => {
useCartStore.setState({
items: [
{ id: "1", name: "Widget", price: 10, quantity: 2 },
{ id: "2", name: "Gadget", price: 25, quantity: 1 },
],
});
expect(useCartStore.getState().total()).toBe(45);
});
it("clears the cart", () => {
useCartStore.setState({
items: [{ id: "1", name: "Widget", price: 10, quantity: 1 }],
});
useCartStore.getState().clearCart();
expect(useCartStore.getState().items).toHaveLength(0);
});
});// src/components/cart-summary.tsx
"use client";
import { useCartStore } from "@/stores/cart";
export function CartSummary() {
const items = useCartStore((s) => s.items);
const total = useCartStore((s) => s.total);
const removeItem = useCartStore((s) => s.removeItem);
if (items.length === 0) {
return <p>Your cart is empty.</p>;
}
return (
<div>
<h2>Cart ({items.length} items)</h2>
<ul>
{items.map((item) => (
<li key={item.id}>
{item.name} x{item.quantity} - ${(item.price * item.quantity).toFixed(2)}
<button
onClick={() => removeItem(item.id)}
aria-label={`Remove ${item.name}`}
>
Remove
</button>
</li>
))}
</ul>
<p>Total: ${total().toFixed(2)}</p>
</div>
);
}// src/components/cart-summary.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, beforeEach } from "vitest";
import { useCartStore } from "@/stores/cart";
import { CartSummary } from "./cart-summary";
describe("CartSummary", () => {
const user = userEvent.setup();
beforeEach(() => {
useCartStore.setState({ items: [] });
});
it("shows empty message when cart is empty", () => {
render(<CartSummary />);
expect(screen.getByText(/your cart is empty/i)).toBeInTheDocument();
});
it("renders cart items", () => {
useCartStore.setState({
items: [
{ id: "1", name: "Widget", price: 10, quantity: 2 },
{ id: "2", name: "Gadget", price: 25, quantity: 1 },
],
});
render(<CartSummary />);
expect(screen.getByText(/widget x2/i)).toBeInTheDocument();
expect(screen.getByText(/\$20\.00/)).toBeInTheDocument();
expect(screen.getByText(/gadget x1/i)).toBeInTheDocument();
expect(screen.getByText(/total: \$45\.00/i)).toBeInTheDocument();
});
it("removes item on button click", async () => {
useCartStore.setState({
items: [{ id: "1", name: "Widget", price: 10, quantity: 1 }],
});
render(<CartSummary />);
await user.click(screen.getByRole("button", { name: /remove widget/i }));
expect(screen.getByText(/your cart is empty/i)).toBeInTheDocument();
});
});O que isso demonstra:
getState() e setState()wrapper em render() envolve o componente de teste em provedores sem a necessidade de fazê-lo em cada teste.useCartStore.setState() define diretamente o estado do store sem acionar ações, útil para configuração de testes.useCartStore.getState() lê o estado atual de forma síncrona, útil para testes unitários em nível de store.Arquivo de utilitários de teste reutilizáveis:
// src/test/utils.tsx
import { render, RenderOptions } from "@testing-library/react";
import { ThemeProvider } from "@/context/theme-context";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
interface CustomRenderOptions extends Omit<RenderOptions, "wrapper"> {
theme?: "light" | "dark";
}
export function renderWithProviders(
ui: React.ReactElement,
{ theme = "light", ...options }: CustomRenderOptions = {}
) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return render(ui, {
wrapper: ({ children }) => (
<QueryClientProvider client={queryClient}>
<ThemeProvider initialTheme={theme}>{children}</ThemeProvider>
</QueryClientProvider>
),
...options,
});
}
// Re-exportar tudo da Testing Library
export * from "@testing-library/react";
export { default as userEvent } from "@testing-library/user-event";// Em arquivos de teste, importe de seus utilitários em vez disso
import { renderWithProviders, screen, userEvent } from "@/test/utils";Testando Zustand com middleware (persist, devtools):
// Stores com middleware de persistência precisam de mock de localStorage
beforeEach(() => {
localStorage.clear();
useCartStore.setState({ items: [] });
});// Tipando opções de render customizado
interface CustomRenderOptions extends Omit<RenderOptions, "wrapper"> {
theme?: "light" | "dark";
initialCartItems?: CartItem[];
}
// Tipando componentes wrapper
function Wrapper({ children }: { children: React.ReactNode }) {
return <ThemeProvider>{children}</ThemeProvider>;
}Vazamento de estado Zustand entre testes -- Stores Zustand são singletons em nível de módulo. O estado definido em um teste persiste para o próximo. Correção: Chame useCartStore.setState({ items: [] }) em beforeEach.
Wrapper de provider ausente -- Componentes que usam useContext lançam erro se nenhum provider for encontrado. Correção: Sempre use um render customizado que envolva os providers necessários.
Testando contexto isoladamente vs. em integração -- Testar o provider e o consumidor separadamente pode perder bugs de integração. Correção: Teste-os juntos com o provider real sempre que possível.
Vazamento de QueryClient entre testes -- O cache do React Query persiste se você reutilizar o mesmo QueryClient. Correção: Crie um novo QueryClient no wrapper para cada teste, com retry: false para evitar timeouts de teste.
Assinaturas Zustand não atualizando em testes -- Se você modificar o store fora do React (por exemplo, getState().addItem()), os componentes podem não re-renderizar. Correção: Use act() em torno das modificações do store que devem acionar re-renderizações, ou defina o estado antes de renderizar.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| Mockar o hook de contexto | Você quer testar um componente isoladamente sem o provider real | Você quer testar a lógica do provider |
Testes unitários Zustand getState() | Você quer testar a lógica do store sem renderizar componentes | Você precisa verificar as reações da UI às mudanças de estado |
| Testes de integração | Você quer testar múltiplos componentes compartilhando estado juntos | Testes unitários rápidos e isolados são suficientes |
| Injeção de dependência via props | O componente pode aceitar valores como props em vez de ler do contexto | O componente é aninhado profundamente e a passagem de props é impraticável |
function renderWithProviders(ui: React.ReactElement) {
return render(ui, {
wrapper: ({ children }) => (
<ThemeProvider><AuthProvider>{children}</AuthProvider></ThemeProvider>
),
});
}Stores Zustand são singletons em nível de módulo. O estado definido em um teste persiste para o próximo no mesmo arquivo. Sempre reinicie o estado em beforeEach:
beforeEach(() => {
useCartStore.setState({ items: [] });
});Use getState() e setState() diretamente:
useCartStore.getState().addItem({ id: "1", name: "W", price: 10 });
expect(useCartStore.getState().items).toHaveLength(1);Ele lança um erro. Sempre envolva componentes dependentes de contexto em seu provider necessário, seja diretamente ou através de um wrapper de render customizado.
Renderize o consumidor dentro do provider real e interaja com ele:
renderWithTheme(<ThemeToggle />);
await user.click(screen.getByRole("button"));
expect(screen.getByRole("button")).toHaveTextContent("Switch to light");Crie um novo QueryClient no wrapper para cada teste com retry: false:
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});Defina o estado do store antes de renderizar:
useCartStore.setState({
items: [{ id: "1", name: "Widget", price: 10, quantity: 2 }],
});
render(<CartSummary />);
expect(screen.getByText(/widget x2/i)).toBeInTheDocument();Modificar o store via getState().addItem() fora de uma renderização React pode não acionar re-renderizações. Use act() em torno das modificações do store, ou defina o estado antes de renderizar.
interface CustomRenderOptions extends Omit<RenderOptions, "wrapper"> {
theme?: "light" | "dark";
initialCartItems?: CartItem[];
}Teste-os juntos com o provider real sempre que possível. Testar separadamente pode perder bugs de integração. Mockar apenas o hook de contexto se você precisar de isolamento real.
Limpe o localStorage antes de cada teste:
beforeEach(() => {
localStorage.clear();
useCartStore.setState({ items: [] });
});Sim:
// src/test/utils.tsx
export * from "@testing-library/react";
export { default as userEvent } from "@testing-library/user-event";
export { renderWithProviders };Em seguida, importe de @/test/utils em todos os arquivos de teste.
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥