kb.erickguedes.com
React: Construindo Interfaces Modernas

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

QueryRetornaQuando usar
getBy*Elemento ou erroElemento existe no DOM
queryBy*Elemento ou nullElemento 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.