Testing Library — testar como o usuário usa.
Renderização, queries por papel/texto/label, simulação de interação com userEvent. Princípio de testar como o usuário usa.
Funções puras são fáceis de testar: input, output, done. Componentes React são diferentes — eles renderizam no DOM, respondem a interações, têm estado interno e efeitos colaterais. Testing Library fornece as ferramentas para testar componentes da forma que importa: como o usuário os usa.
Instalação e setup
npm install -D @testing-library/react @testing-library/user-event @testing-library/jest-dom import "@testing-library/jest-dom";
// adiciona: toBeInTheDocument, toBeVisible, toHaveValue, toBeDisabled, etc. export default defineConfig({
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./src/test/setup.ts"],
},
}); Renderizar e fazer queries
render monta o componente em um DOM simulado (jsdom). O objeto screen permite encontrar elementos como o usuário os vê:
import { render, screen } from "@testing-library/react";
import { ArticleCard } from "./ArticleCard";
import type { Artigo } from "../types";
const artigoFixture: Artigo = {
id: 1,
titulo: "CSS Grid — guia completo",
descricao: "Como usar CSS Grid para criar layouts complexos.",
autor: { nome: "Ana Silva" },
tags: ["css", "layout"],
curtidas: 42,
publicadoEm: "2024-03-15T00:00:00Z",
};
describe("ArticleCard", () => {
it("renderiza o título do artigo", () => {
render(<ArticleCard artigo={artigoFixture} curtido={false} onCurtir={() => {}} />);
// getByRole para headings, buttons, links — mais próximo de como o usuário vê
expect(screen.getByRole("heading", { name: "CSS Grid — guia completo" })).toBeInTheDocument();
});
it("renderiza o nome do autor", () => {
render(<ArticleCard artigo={artigoFixture} curtido={false} onCurtir={() => {}} />);
expect(screen.getByText("Ana Silva")).toBeInTheDocument();
});
it("renderiza todas as tags do artigo", () => {
render(<ArticleCard artigo={artigoFixture} curtido={false} onCurtir={() => {}} />);
expect(screen.getByText("css")).toBeInTheDocument();
expect(screen.getByText("layout")).toBeInTheDocument();
});
it("exibe ❤️ quando curtido e 🤍 quando não curtido", () => {
const { rerender } = render(
<ArticleCard artigo={artigoFixture} curtido={false} onCurtir={() => {}} />
);
expect(screen.getByRole("button", { name: /curtir/i })).toHaveTextContent("🤍");
rerender(<ArticleCard artigo={artigoFixture} curtido={true} onCurtir={() => {}} />);
expect(screen.getByRole("button", { name: /curtido/i })).toHaveTextContent("❤️");
});
}); A hierarquia de queries
Testing Library recomenda uma ordem de preferência para queries — da mais próxima da experiência do usuário à mais técnica:
// 1ª escolha — papel de acessibilidade (como leitores de tela veem)
screen.getByRole("button", { name: "Curtir" });
screen.getByRole("heading", { name: "CSS Grid" });
screen.getByRole("textbox", { name: "E-mail" });
// 2ª escolha — label de input (acessibilidade)
screen.getByLabelText("E-mail");
// 3ª escolha — texto visível
screen.getByText("Ana Silva");
screen.getByText(/há \d+ minutos/); // regex para texto parcial
// 4ª escolha — atributo alt (imagens)
screen.getByAltText("Foto do autor");
// 5ª escolha — placeholder
screen.getByPlaceholderText("Buscar artigos...");
// evitar — só quando as anteriores não funcionam
screen.getByTestId("botao-curtir"); // depende de data-testid — não reflete uso real getBy* vs queryBy* vs findBy*
// getBy* — lança erro se não encontrar — para asserções de presença
const botao = screen.getByRole("button", { name: "Curtir" });
// Se não existir: TestingLibraryElementError — útil como erro imediato
// queryBy* — retorna null se não encontrar — para asserções de ausência
const badge = screen.queryByText("Novo");
expect(badge).not.toBeInTheDocument(); // verificar que algo NÃO está na tela
// findBy* — assíncrono, aguarda aparecer — para conteúdo que chega depois
const titulo = await screen.findByText("CSS Grid");
// ideal para artigos que chegam após fetch Simular interações com userEvent
userEvent simula a sequência completa de eventos do usuário — hover, focus, keydown, keyup, click. Mais fiel ao comportamento real que fireEvent, que dispara apenas um evento DOM:
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LikeButton } from "./LikeButton";
describe("LikeButton", () => {
it("chama onCurtir ao ser clicado", async () => {
const user = userEvent.setup(); // instância com configurações
const onCurtir = vi.fn();
render(<LikeButton curtido={false} contagem={5} onCurtir={onCurtir} />);
await user.click(screen.getByRole("button"));
expect(onCurtir).toHaveBeenCalledOnce();
});
it("exibe o número de curtidas corretamente", () => {
render(<LikeButton curtido={false} contagem={42} onCurtir={() => {}} />);
expect(screen.getByRole("button")).toHaveTextContent("42");
});
it("tem aria-pressed true quando curtido", () => {
render(<LikeButton curtido={true} contagem={43} onCurtir={() => {}} />);
expect(screen.getByRole("button")).toHaveAttribute("aria-pressed", "true");
});
}); Testando formulários
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { NewsletterForm } from "./NewsletterForm";
describe("NewsletterForm", () => {
it("habilita o botão apenas com email preenchido", async () => {
const user = userEvent.setup();
render(<NewsletterForm onSubmit={() => {}} />);
const botao = screen.getByRole("button", { name: "Inscrever" });
expect(botao).toBeDisabled(); // começa desabilitado
await user.type(screen.getByLabelText("E-mail"), "ana@example.com");
expect(botao).not.toBeDisabled(); // habilitado após digitar
});
it("chama onSubmit com o email ao submeter", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<NewsletterForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText("E-mail"), "ana@example.com");
await user.click(screen.getByRole("button", { name: "Inscrever" }));
expect(onSubmit).toHaveBeenCalledWith("ana@example.com");
});
it("limpa o campo após envio bem-sucedido", async () => {
const user = userEvent.setup();
render(<NewsletterForm onSubmit={() => {}} />);
const input = screen.getByLabelText("E-mail");
await user.type(input, "ana@example.com");
await user.click(screen.getByRole("button", { name: "Inscrever" }));
expect(input).toHaveValue(""); // campo limpo após envio
});
}); Matchers do @testing-library/jest-dom
// presença no documento
expect(elemento).toBeInTheDocument();
expect(elemento).not.toBeInTheDocument();
// visibilidade
expect(elemento).toBeVisible();
expect(elemento).not.toBeVisible(); // quando display:none ou visibility:hidden
// estado de formulário
expect(input).toHaveValue("ana@example.com");
expect(botao).toBeDisabled();
expect(botao).not.toBeDisabled();
expect(checkbox).toBeChecked();
// atributos
expect(botao).toHaveAttribute("aria-pressed", "true");
expect(link).toHaveAttribute("href", "/artigos/css");
// conteúdo de texto
expect(elemento).toHaveTextContent("CSS Grid");
expect(elemento).toHaveTextContent(/\d+ curtidas/); // regex Resumo
- Testing Library: renderiza componentes em jsdom e expõe
screenpara fazer queries como o usuário faz — por papel, texto, label. - Hierarquia de queries:
getByRole>getByLabelText>getByText. EvitegetByTestId— ele não reflete como o usuário usa a interface. getBy*lança erro se não encontrar;queryBy*retornanull(para asserções de ausência);findBy*é assíncrono (para conteúdo que aparece depois).userEvent.setup()simula interações reais — click, type, press. Mais fiel ao browser quefireEvent.@testing-library/jest-domadiciona matchers expressivos:toBeInTheDocument,toBeDisabled,toHaveValue,toHaveAttribute.
Qual é a filosofia central da Testing Library?
Por que getByRole é preferível a getByTestId?
Qual a diferença entre getBy*, queryBy* e findBy*?
Por que usar userEvent.setup() em vez de fireEvent para simular interações?
Aula concluída
Quase lá.