Testando Comportamento Assíncrono
Teste estados de carregamento, tratamento de erros, busca de dados, limites do Suspense e operações com tempo.
Busque em todas as páginas da documentação
Teste estados de carregamento, tratamento de erros, busca de dados, limites do Suspense e operações com tempo.
🤖 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";
// Aguarda um elemento aparecer
const heading = await screen.findByText(/welcome/i); // espera até 1s
// Aguarda uma condição
await waitFor(() => {
expect(screen.getByText(/loaded/i)).toBeInTheDocument();
});
// Aguarda um elemento desaparecer
await waitFor(() => {
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
// Timeout personalizado
const result = await screen.findByText(/data/i, {}, { timeout: 3000 });
// Timers falsos para setTimeout/setInterval
vi.useFakeTimers();
render(<Countdown seconds={10} />);
act(() => vi.advanceTimersByTime(5000));
expect(screen.getByText("5 seconds left")).toBeInTheDocument();
vi.useRealTimers();Quando usar isso: Ao testar qualquer componente que busca dados, exibe spinners de carregamento, lida com erros ou usa timers.
// src/components/data-table.tsx
"use client";
import { useEffect, useState } from "react";
interface Product {
id: number;
name: string;
price: number;
}
export function DataTable() {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function loadProducts() {
try {
const res = await fetch("/api/products");
if (!res.ok) throw new Error("Failed to load products");
const data: Product[] = await res.json();
setProducts(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setLoading(false);
}
}
loadProducts();
}, []);
if (loading) {
return (
<div role="status" aria-label="Loading">
<p>Loading products...</p>
</div>
);
}
if (error) {
return <p role="alert">Error: {error}</p>;
}
if (products.length === 0) {
return <p>No products found.</p>;
}
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{products.map((product) => (
<tr key={product.id}>
<td>{product.name}</td>
<td>${product.price.toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
);
}// src/components/data-table.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { DataTable } from "./data-table";
const products = [
{ id: 1, name: "Widget", price: 9.99 },
{ id: 2, name: "Gadget", price: 24.5 },
{ id: 3, name: "Doohickey", price: 4.0 },
];
const server = setupServer(
http.get("/api/products", () => {
return HttpResponse.json(products);
})
);
beforeEach(() => server.listen());
afterEach(() => {
server.resetHandlers();
server.close();
});
describe("DataTable", () => {
it("shows loading state initially", () => {
render(<DataTable />);
expect(screen.getByRole("status", { name: /loading/i })).toBeInTheDocument();
});
it("renders products on success", async () => {
render(<DataTable />);
// Wait for loading to finish
await waitFor(() => {
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
// Verify table content
expect(screen.getByText("Widget")).toBeInTheDocument();
expect(screen.getByText("$9.99")).toBeInTheDocument();
expect(screen.getByText("Gadget")).toBeInTheDocument();
expect(screen.getByText("$24.50")).toBeInTheDocument();
// Verify row count
const rows = screen.getAllByRole("row");
expect(rows).toHaveLength(4); // 1 header + 3 data rows
});
it("shows error message on failure", async () => {
server.use(
http.get("/api/products", () => {
return HttpResponse.json(null, { status: 500 });
})
);
render(<DataTable />);
const alert = await screen.findByRole("alert");
expect(alert).toHaveTextContent("Failed to load products");
});
it("shows empty state when no products", async () => {
server.use(
http.get("/api/products", () => {
return HttpResponse.json([]);
})
);
render(<DataTable />);
expect(await screen.findByText(/no products found/i)).toBeInTheDocument();
});
it("shows loading then transitions to data", async () => {
render(<DataTable />);
// Loading is visible
expect(screen.getByText(/loading products/i)).toBeInTheDocument();
// Data appears and loading disappears
await screen.findByText("Widget");
expect(screen.queryByText(/loading products/i)).not.toBeInTheDocument();
});
});O que isso demonstra:
server.use() para substituições de manipuladores por testefindByRole e waitFor para transições assíncronasfindBy são uma combinação de getBy + waitFor -- elas tentam novamente até que o elemento apareça ou o tempo limite expire.waitFor pesquisa seu callback em intervalos (padrão de 50ms) até que ele passe ou o tempo limite expire (padrão de 1000ms).fetch no nível da rede -- seu código de componente faz chamadas fetch reais que o MSW captura.server.use() adiciona manipuladores de solicitação que têm prioridade sobre os padrão -- chame server.resetHandlers() em afterEach para remover substituições.findBy em act() para que as atualizações de estado do React sejam processadas.Testando limites do Suspense:
// src/components/user-data.tsx
import { Suspense } from "react";
async function fetchUser(id: number) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
// Este componente suspende enquanto os dados carregam
function UserName({ userPromise }: { userPromise: Promise<{ name: string }> }) {
const user = use(userPromise);
return <span>{user.name}</span>;
}
export function UserCard({ userId }: { userId: number }) {
const userPromise = fetchUser(userId);
return (
<Suspense fallback={<p>Loading user...</p>}>
<UserName userPromise={userPromise} />
</Suspense>
);
}it("shows fallback then user name", async () => {
server.use(
http.get("/api/users/1", async () => {
await delay(100); // simula latência de rede
return HttpResponse.json({ name: "Alice" });
})
);
render(<UserCard userId={1} />);
expect(screen.getByText("Loading user...")).toBeInTheDocument();
expect(await screen.findByText("Alice")).toBeInTheDocument();
});Timers falsos para setTimeout/setInterval:
// src/components/auto-save.tsx
"use client";
import { useEffect, useState } from "react";
export function AutoSave({ onSave }: { onSave: () => Promise<void> }) {
const [status, setStatus] = useState("idle");
useEffect(() => {
const interval = setInterval(async () => {
setStatus("saving");
await onSave();
setStatus("saved");
}, 30000);
return () => clearInterval(interval);
}, [onSave]);
return <p>{status === "saving" ? "Saving..." : status === "saved" ? "Saved" : ""}</p>;
}import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
describe("AutoSave", () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it("saves every 30 seconds", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
render(<AutoSave onSave={onSave} />);
// Avança após o primeiro intervalo
await act(async () => {
vi.advanceTimersByTime(30000);
});
expect(onSave).toHaveBeenCalledTimes(1);
// Avança outro intervalo
await act(async () => {
vi.advanceTimersByTime(30000);
});
expect(onSave).toHaveBeenCalledTimes(2);
});
});// Tipa a resposta do manipulador MSW para segurança
import { http, HttpResponse } from "msw";
interface Product {
id: number;
name: string;
price: number;
}
http.get("/api/products", () => {
return HttpResponse.json<Product[]>([
{ id: 1, name: "Widget", price: 9.99 },
]);
});waitFor com efeitos colaterais -- O callback em waitFor é executado várias vezes. Não coloque userEvent clicks ou outros efeitos colaterais dentro dele. Correção: Coloque apenas asserções dentro de waitFor.
Timers falsos quebrando waitFor -- vi.useFakeTimers() pode congelar o intervalo de pesquisa do waitFor. Correção: Avance os timers dentro de act() antes de waitFor, ou use vi.useFakeTimers({ shouldAdvanceTime: true }).
Não limpar o servidor MSW -- Esquecer server.resetHandlers() faz com que as substituições de manipuladores vazem entre os testes. Correção: Sempre chame server.resetHandlers() em afterEach.
Testando atualizações de estado assíncronas sem esperar -- Asserções síncronas após render() veem o estado inicial, não o estado resolvido. Correção: Use findBy ou waitFor para todas as asserções assíncronas.
Múltiplos blocos waitFor que poderiam ser um -- Cada waitFor pesquisa independentemente, adicionando tempo desnecessário ao teste. Correção: Combine asserções relacionadas em um único waitFor:
// Lento: duas chamadas waitFor separadas
await waitFor(() => expect(a).toBeInTheDocument());
await waitFor(() => expect(b).toBeInTheDocument());
// Rápido: um waitFor com ambas as asserções
await waitFor(() => {
expect(a).toBeInTheDocument();
expect(b).toBeInTheDocument();
});| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| MSW | Você quer mocking realista em nível de rede que funcione com qualquer biblioteca fetch | Você precisa fazer mock de operações assíncronas não HTTP |
vi.stubGlobal("fetch") | Mocking rápido de fetch pontual sem configuração MSW | Você tem muitos endpoints de API para fazer mock (MSW é mais fácil de manter) |
| Playwright | Você precisa testar fluxos de UI assíncronos em um navegador real | Testes assíncronos rápidos em nível de unidade são suficientes |
| Utilitários de teste React Suspense | Você testa streaming do lado do servidor ou RSC | Seus componentes usam fetching baseado em useEffect |
findBy é um atalho para getBy + waitFor -- ele espera que um elemento apareça no DOM.waitFor tenta novamente qualquer callback de asserção até que ele passe ou expire.findBy ao esperar por um elemento; use waitFor para asserções não relacionadas a elementos.await waitFor(() => {
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});O callback em waitFor é executado várias vezes (pesquisa). Colocar userEvent.click() ou outros efeitos colaterais dentro dele acionaria essas ações repetidamente. Coloque apenas asserções dentro de waitFor.
http.get(), http.post(), etc.setupServer(...handlers).server.use() em testes individuais para substituir respostas.server.resetHandlers() em afterEach.vi.useFakeTimers();
render(<AutoSave onSave={onSave} />);
await act(async () => {
vi.advanceTimersByTime(30000);
});
expect(onSave).toHaveBeenCalledTimes(1);
vi.useRealTimers();Timers falsos impedem que o intervalo de pesquisa do waitFor avance. Corrija avançando os timers dentro de act() antes de usar waitFor, ou use vi.useFakeTimers({ shouldAdvanceTime: true }).
Renderize o componente e afirme o fallback primeiro, depois espere pelo conteúdo resolvido:
render(<UserCard userId={1} />);
expect(screen.getByText("Loading user...")).toBeInTheDocument();
expect(await screen.findByText("Alice")).toBeInTheDocument();Use um único waitFor com todas as asserções em vez de múltiplos waitFor separados:
await waitFor(() => {
expect(a).toBeInTheDocument();
expect(b).toBeInTheDocument();
});http.get("/api/products", () => {
return HttpResponse.json<Product[]>([
{ id: 1, name: "Widget", price: 9.99 },
]);
});Ambos têm um padrão de 1000ms. Você pode personalizá-lo:
await screen.findByText(/data/i, {}, { timeout: 3000 });Sem isso, as substituições de manipuladores adicionadas via server.use() vazam entre os testes, causando comportamento inesperado nos testes subsequentes.
Afirme que o carregamento está visível imediatamente após a renderização, depois use findBy para esperar pelos dados e afirmar que o carregamento desapareceu:
render(<DataTable />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await screen.findByText("Widget");
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥