Testando componentes React
TL;DR
O fluxo é:
render(<Componente />)monta num DOM (jsdom), você busca elementos com as queries doscreen(nota 07), simula a interação comuser-evente afirma o resultado visível. UseuserEvent.setup()(não ofireEvent) porque ele simula interações reais — foco, teclas, eventos na ordem certa. Interações e efeitos são assíncronos, entãoawaitosuserEvente usefindBypara o que aparece depois. Teste comportamento (o que o usuário vê e faz), não props/estado interno.renderlimpa sozinho entre testes na config moderna.
O problema: como testar um botão que abre um menu?
Você tem um componente: clica no botão, um menu aparece; clica num item, um callback dispara. Como testar isso sem abrir um browser? E, mais sutil: como testar sem amarrar o teste aos detalhes internos (o state isOpen, o nome do handler), para ele sobreviver a refatorações?
A resposta junta as duas notas anteriores: renderizar o componente num DOM simulado, interagir como um usuário (via Testing Library), e afirmar sobre o que fica visível. Esta nota é o fluxo concreto — o gesto que você repete em todo teste de componente.
O fluxo básico: render → interagir → afirmar
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect, test } from 'vitest';
import { Contador } from './Contador';
test('incrementa ao clicar', async () => {
const user = userEvent.setup(); // 1. prepara a simulação de usuário
render(<Contador />); // 2. monta no jsdom
const botao = screen.getByRole('button', { name: /incrementar/i });
expect(screen.getByText('Total: 0')).toBeInTheDocument();
await user.click(botao); // 3. interage (assíncrono!)
expect(screen.getByText('Total: 1')).toBeInTheDocument(); // 4. afirma o visível
});As quatro peças:
render(jsx)monta o componente num DOM (oenvironment: 'jsdom'da nota 02). Retorna utilidades, mas prefira oscreenglobal para as queries.screen.getByRole(...)encontra os elementos pelo que o usuário percebe (nota 07).userEventsimula a interação.expect(...).toBeInTheDocument()— matcher do@testing-library/jest-dom, que estende oexpectcom asserções de DOM (toBeVisible,toHaveValue,toBeDisabled…).
user-event vs fireEvent: use user-event
Há duas formas de simular interação, e a escolha importa:
fireEvent | userEvent | |
|---|---|---|
| O que faz | dispara um evento DOM cru (click) | simula a interação completa do usuário |
| Realismo | baixo (só o evento pedido) | alto (hover → focus → keydown → keyup → click, na ordem) |
| Digitar | fireEvent.change seta o valor de uma vez | userEvent.type dispara tecla por tecla |
| Recomendação | evite | padrão |
const user = userEvent.setup();
await user.type(screen.getByLabelText('E-mail'), 'ana@ex.com'); // tecla por tecla
await user.click(screen.getByRole('button', { name: /entrar/i }));
await user.keyboard('{Enter}');userEvent é mais fiel porque um clique real não é só um evento click — é uma cascata (mousedown, focus, mouseup, click) que seu componente pode depender. fireEvent pula isso e pode passar num teste que falharia com um usuário de verdade.
Esquecer o
awaitnouserEventO que acontece: você faz
user.click(botao)semawait, e a asserção seguinte falha porque a UI ainda não atualizou — ou o teste vira flaky. Por quê: desde a v14, os métodos douserEventsão assíncronos (retornam promise), para simular a interação de forma realista incluindo atualizações de estado. Semawait, você afirma antes da UI reagir. Como evitar: sempreawaitos métodos douserEvent(e por isso o teste éasync). Também não esqueça ouserEvent.setup()no início — a API v14 exige.
UI assíncrona: findBy e waitFor
Componentes que carregam dados (um fetch no useEffect) mostram o conteúdo depois. Aí entra o findBy (nota 07), que espera o elemento aparecer:
test('mostra os pedidos após carregar', async () => {
render(<ListaPedidos />);
// enquanto carrega:
expect(screen.getByText(/carregando/i)).toBeInTheDocument();
// findBy espera o elemento surgir (após o fetch resolver):
expect(await screen.findByText('Pedido #42')).toBeInTheDocument();
// e o "carregando" sumiu:
expect(screen.queryByText(/carregando/i)).not.toBeInTheDocument();
});findBy (para “apareceu”) e waitFor (para “esta asserção passa a valer em algum momento”) cobrem o assíncrono de UI. De onde vêm os dados desse fetch num teste? De um mock de rede — o assunto da próxima nota (MSW).
O que eu devo (e não devo) testar num componente?
Teste o comportamento que o usuário observa: renderiza o conteúdo certo dadas as props? responde à interação (clique, digitação) mudando o que aparece? mostra estados de loading/erro/vazio? chama os callbacks certos (
onSubmit) com os dados certos? Não teste implementação: o valor de umuseState, se umuseEffectrodou, nomes de handlers, estrutura de markup irrelevante. O teste da Testes 06 vale integralmente aqui — se você renomear um state ou trocaruseStateporuseReducersem mudar o que o usuário vê, o teste deve continuar verde. Se ele quebra, estava testando a coisa errada. Uma boa heurística: se a asserção menciona algo que o usuário não consegue perceber, reconsidere.
Testando componentes React em uma frase: render monta no jsdom, userEvent.setup() + await user.click/type simula a interação real, as queries do screen acham os elementos pelo que o usuário vê, findBy espera a UI assíncrona, e você afirma sempre sobre o comportamento visível — nunca sobre state ou props internos.
Em entrevista
“The flow is render, interact, assert. I
renderthe component into jsdom, query elements withscreenby role or label, simulate interaction withuserEvent— notfireEvent, becauseuserEventreproduces the full real interaction, focus, keystrokes, in order. Since v14 those methods are async, so Iawait user.click. For UI that loads data I usefindBy, which waits for the element to appear. And I test behavior — what the user sees and does — never internal state or props, so the test survives refactors like swappinguseStateforuseReducer.”
| PT | EN |
|---|---|
| Renderizar | To render |
| Evento de usuário | User event |
| Interação completa | Full interaction |
| UI assíncrona | Async UI |
| Estado de carregamento | Loading state |
| Comportamento observável | Observable behavior |
O que vem a seguir
O teste de componente com fetch precisa de dados vindos “da rede” — e chamá-la de verdade é lento e frágil. A solução moderna é interceptar a rede na camada certa, com o MSW.
- 09 — MSW: mockando a rede — interceptar HTTP em testes.
- 10 — Testando hooks e estado — testar a lógica fora do componente.
Fontes
- Testing Library — React Testing Library —
render— montar componentes. - Testing Library — user-event —
setup()e a simulação realista. - Testing Library — jest-dom matchers —
toBeInTheDocument,toHaveValue, etc.