Testes no React
Aula 6 de 8
Setup com Vitest + Testing Library
ash npm install --save-dev vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
` ypescript // vitest.config.ts import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react";
export default defineConfig({ plugins: [react()], test: { environment: "jsdom", globals: true, setupFiles: "./src/test/setup.ts", }, }); `
ypescript // src/test/setup.ts import "@testing-library/jest-dom";
json { "scripts": { "test": "vitest run", "test:watch": "vitest" } }
render, screen e Queries
` sx import { render, screen } from "@testing-library/react"; import { describe, it, expect } from "vitest";
function Saudacao({ nome }: { nome: string }) { return <h1>Olá, {nome}!</h1>; }
describe("Saudacao", () => { it("deve renderizar a saudação com o nome", () => { render(<Saudacao nome="Alice" />);
expect(screen.getByText("Olá, Alice!")).toBeInTheDocument();
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
"Olá, Alice!"
);
}); }); `
getBy vs findBy vs queryBy
| Query | Retorna | Quando usar |
|---|---|---|
| getBy* | Elemento ou erro | Elemento existe no DOM |
| queryBy* | Elemento ou null | Elemento NÃO existe no DOM |
| indBy* | Promise (elemento) | Elemento aparece após ação assíncrona |
` sx // findBy — para elementos assíncronos it("deve carregar dados", async () => { render(<ComponenteAssincrono />); expect(await screen.findByText("Carregado!")).toBeInTheDocument(); });
// queryBy — para verificar ausência it("não deve mostrar erro inicialmente", () => { render(<Formulario />); expect(screen.queryByText("Erro")).not.toBeInTheDocument(); }); `
fireEvent vs userEvent
` sx import userEvent from "@testing-library/user-event";
function Contador() { const [count, setCount] = useState(0); return ( <div> <p>Contagem: {count}</p> <button onClick={() => setCount(count + 1)}>Incrementar</button> </div> ); }
it("deve incrementar (userEvent)", async () => { const user = userEvent.setup(); render(<Contador />);
await user.click(screen.getByText("Incrementar")); expect(screen.getByText("Contagem: 1")).toBeInTheDocument(); }); `
Mock de Hooks
` sx import { vi } from "vitest";
vi.mock("../hooks/useAuth", () => ({ useAuth: () => ({ usuario: { nome: "Alice", role: "admin" }, isLoading: false, }), }));
global.fetch = vi.fn();
it("deve fazer requisição", async () => { (global.fetch as any).mockResolvedValueOnce({ ok: true, json: async () => ({ id: 1, nome: "Alice" }), });
render(<Componente />); expect(await screen.findByText("Alice")).toBeInTheDocument(); }); `
Mock de API com MSW
ash npm install --save-dev msw
` ypescript // src/test/mocks/handlers.ts import { http, HttpResponse } from "msw";
export const handlers = [ http.get("/api/usuarios", () => { return HttpResponse.json([ { id: 1, nome: "Alice" }, { id: 2, nome: "Bob" }, ]); }),
http.post("/api/usuarios", async ({ request }) => { const body = await request.json(); return HttpResponse.json({ id: 3, ...(body as object) }, { status: 201 }); }), ]; `
` ypescript // src/test/mocks/server.ts import { setupServer } from "msw/node"; import { handlers } from "./handlers";
export const server = setupServer(...handlers); `
` ypescript // src/test/setup.ts import { server } from "./mocks/server"; import "@testing-library/jest-dom";
beforeAll(() => server.listen({ onUnhandledRequest: "warn" })); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); `
` sx it("deve listar usuários", async () => { render(<ListaUsuarios />);
expect(await screen.findByText("Alice")).toBeInTheDocument(); expect(screen.getByText("Bob")).toBeInTheDocument(); }); `
Testing Custom Hooks
` sx import { renderHook, act } from "@testing-library/react";
function useContador(valorInicial = 0) { const [contador, setContador] = useState(valorInicial); const incrementar = () => setContador((p) => p + 1); return { contador, incrementar }; }
describe("useContador", () => { it("deve iniciar com valor padrão", () => { const { result } = renderHook(() => useContador()); expect(result.current.contador).toBe(0); });
it("deve incrementar", () => { const { result } = renderHook(() => useContador(10));
act(() => {
result.current.incrementar();
});
expect(result.current.contador).toBe(11);
}); }); `
Testing Formulários
` sx function LoginForm() { const [email, setEmail] = useState(""); const [senha, setSenha] = useState("");
const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!email || !senha) return; console.log("Login realizado"); };
return ( <form onSubmit={handleSubmit}> <input aria-label="Email" value={email} onChange={(e) => setEmail(e.target.value)} /> <input type="password" aria-label="Senha" value={senha} onChange={(e) => setSenha(e.target.value)} /> <button type="submit">Entrar</button> </form> ); } `
` sx import userEvent from "@testing-library/user-event";
it("deve submeter o formulário", async () => { const user = userEvent.setup(); render(<LoginForm />);
await user.type(screen.getByLabelText("Email"), "[email protected]"); await user.type(screen.getByLabelText("Senha"), "123456"); await user.click(screen.getByText("Entrar")); }); `
Coverage
json // vitest.config.ts { "test": { "coverage": { "provider": "v8", "reporter": ["text", "html", "lcov"], "include": ["src/**/*.{ts,tsx}"], "exclude": ["src/**/*.test.*", "src/test/**"] } } }
ash npx vitest run --coverage
Lab: Testar Componente de Lista
` sx interface ItemLista { id: number; texto: string; concluido: boolean; }
function ListaTarefas({ items, onToggle }: { items: ItemLista[]; onToggle: (id: number) => void; }) { return ( <ul> {items.map((item) => ( <li key={item.id}> <label> <input type="checkbox" checked={item.concluido} onChange={() => onToggle(item.id)} /> {item.texto} </label> </li> ))} </ul> ); } `
` sx import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, it, expect, vi } from "vitest";
describe("ListaTarefas", () => { const items = [ { id: 1, texto: "Estudar React", concluido: false }, { id: 2, texto: "Fazer testes", concluido: true }, ];
it("deve renderizar todas as tarefas", () => { render(<ListaTarefas items={items} onToggle={() => {}} />); expect(screen.getByText("Estudar React")).toBeInTheDocument(); expect(screen.getByText("Fazer testes")).toBeInTheDocument(); });
it("deve chamar onToggle ao clicar no checkbox", async () => { const onToggle = vi.fn(); const user = userEvent.setup();
render(<ListaTarefas items={items} onToggle={onToggle} />);
await user.click(screen.getAllByRole("checkbox")[0]);
expect(onToggle).toHaveBeenCalledWith(1);
});
it("deve marcar tarefas concluídas como checked", () => { render(<ListaTarefas items={items} onToggle={() => {}} />); const checkboxes = screen.getAllByRole("checkbox"); expect(checkboxes[0]).not.toBeChecked(); expect(checkboxes[1]).toBeChecked(); }); }); `
ash npx vitest run
Testes não são opcionais. Testing Library testa comportamento (o que o usuário vê e faz), não implementação. Prefira userEvent sobre fireEvent para simular interações reais.