<h2>Testing Library em Profundidade: Queries, Fire Events e Async</h2>
<p>Testing Library é uma biblioteca de testes que oferece uma abordagem centrada no usuário. Em vez de testar implementações internas (como estado local ou props), testamos o comportamento real que o usuário experimenta. Essa filosofia mudou fundamentalmente como a comunidade React escreve testes, tornando-os mais robustos e menos frágeis.</p>
<p>Durante minha carreira, vi inúmeros testes quebrarem porque um componente foi refatorado internamente, mesmo que o comportamento visual permanecesse igual. Testing Library resolve isso testando o que importa: o que o usuário vê e interage. Neste artigo, vou guiá-lo através dos três pilares essenciais: queries para encontrar elementos, fire events para simular interações e técnicas assíncronas para lidar com operações que não são instantâneas.</p>
<h2>Queries: A Arte de Encontrar Elementos</h2>
<p>As queries são o alicerce de qualquer teste com Testing Library. Elas definem como você localiza elementos no DOM de forma que se alinhe com a experiência do usuário. Existem diferentes tipos de queries, cada uma com um propósito específico e uma preferência de uso.</p>
<h3>Entendendo a Hierarquia de Queries</h3>
<p>Testing Library organiza suas queries em três categorias: queries que retornam um elemento, queries que retornam múltiplos elementos e queries que lançam erro se nada for encontrado. A hierarquia de preferência é crítica: sempre prefira métodos que retornam resultados acessíveis ao usuário.</p>
<p>A ordem de preferência é: <strong>getByRole</strong> (mais semântico), <strong>getByLabelText</strong> (para formulários), <strong>getByPlaceholderText</strong>, <strong>getByText</strong>, <strong>getByTestId</strong> (menos preferido, apenas para casos extremos). Essa hierarquia existe porque métodos mais altos no topo refletem melhor como um usuário real interage com a interface.</p>
<pre><code class="language-jsx"></code></pre>
<h3>QueryByX vs GetByX vs FindByX</h3>
<p>Essas variações são fundamentais e confundem muitos iniciantes. <strong>GetByX</strong> lança erro se o elemento não existir (melhor para afirmações positivas). <strong>QueryByX</strong> retorna <code>null</code> se nada for encontrado (ideal para testar ausência). <strong>FindByX</strong> é assíncrono e aguarda até que o elemento apareça (essencial para dados dinâmicos).</p>
<pre><code class="language-jsx"></code></pre>
<h3>Using getByRole para Máxima Acessibilidade</h3>
<p><code>getByRole</code> é a query mais poderosa porque força você a pensar em acessibilidade. Todo elemento deve ter um role (papel) semântico. Inputs têm role <code>textbox</code>, botões têm role <code>button</code>, e assim por diante. Usar <code>getByRole</code> garante que seu componente é acessível.</p>
<pre><code class="language-jsx">function ProductCard({ name, onAddToCart }) {
return (
<article>
<h2>{name}</h2>
<button onClick={onAddToCart}>Adicionar ao carrinho</button>
</article>
);
}
test('card de produto está semanticamente correto', () => {
const handleClick = jest.fn();
render(<ProductCard name="Teclado" onAddToCart={handleClick} />);
// getByRole encontra por semantic HTML
const heading = screen.getByRole('heading', { level: 2, name: /teclado/i });
expect(heading).toBeInTheDocument();
const button = screen.getByRole('button', { name: /adicionar ao carrinho/i });
expect(button).toBeInTheDocument();
});</code></pre>
<h2>Fire Events: Simulando Interações do Usuário</h2>
<p>Fire events é como você simula cliques, digitação, submissão de formulários e outras interações. Porém, há uma nuance importante: <code>fireEvent</code> é o método mais baixo nível. Para a maioria dos casos, usar <code>userEvent</code> é mais apropriado porque simula eventos reais que um navegador geraria.</p>
<h3>fireEvent vs userEvent</h3>
<p><code>fireEvent</code> dispara um evento diretamente no elemento. É rápido mas não simula o comportamento real do navegador. <code>userEvent</code>, por outro lado, simula como um usuário real interagiria, gerando múltiplos eventos na sequência correta. Por exemplo, digitar um caractere gera <code>focus</code>, <code>keydown</code>, <code>keyup</code>, <code>input</code> e <code>change</code>. FireEvent dispara apenas <code>change</code>.</p>
<pre><code class="language-jsx"></code></pre>
<h3>Padrão userEvent.setup()</h3>
<p>Com versões recentes de <code>@testing-library/user-event</code>, o padrão mudou. Você precisa chamar <code>userEvent.setup()</code> no início do teste (ou antes de cada teste). Isso cria uma instância que gerencia estado entre eventos.</p>
<pre><code class="language-jsx">import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
function TodoForm({ onAdd }) {
const [input, setInput] = React.useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (input.trim()) {
onAdd(input);
setInput('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Nova tarefa"
/>
<button type="submit">Adicionar</button>
</form>
);
}
test('usuário pode adicionar tarefa', async () => {
const handleAdd = jest.fn();
render(<TodoForm onAdd={handleAdd} />);
const user = userEvent.setup();
const input = screen.getByPlaceholderText('Nova tarefa');
const button = screen.getByRole('button', { name: /adicionar/i });
// userEvent mantém estado entre eventos
await user.type(input, 'Estudar Testing Library');
await user.click(button);
expect(handleAdd).toHaveBeenCalledWith('Estudar Testing Library');
expect(input.value).toBe(''); // Input foi limpo
});</code></pre>
<h3>Testando Eventos Complexos</h3>
<p>Alguns eventos são mais complexos: hover, focus, teclado especial. <code>userEvent</code> fornece métodos específicos para cada um. Conhecê-los evita tentativas fracassadas.</p>
<pre><code class="language-jsx">function DropdownMenu() {
const [open, setOpen] = React.useState(false);
return (
<div>
<button onClick={() => setOpen(!open)}>Menu</button>
{open && (
<menu>
<li><a href="/profile">Perfil</a></li>
<li><a href="/settings">Configurações</a></li>
<li><a href="/logout">Sair</a></li>
</menu>
)}
</div>
);
}
test('interações complexas com dropdown', async () => {
render(<DropdownMenu />);
const user = userEvent.setup();
const button = screen.getByRole('button', { name: /menu/i });
// Menu não deve estar visível inicialmente
expect(screen.queryByRole('menuitem')).not.toBeInTheDocument();
// Click abre o menu
await user.click(button);
expect(screen.getAllByRole('link')).toHaveLength(3);
// Click novamente fecha
await user.click(button);
expect(screen.queryByRole('menuitem')).not.toBeInTheDocument();
// Tab e Enter também funcionam
await user.click(button);
const profileLink = screen.getByRole('link', { name: /perfil/i });
await user.tab();
expect(document.activeElement).toBe(profileLink);
await user.keyboard('{Enter}');
});</code></pre>
<h2>Padrões Assíncronos: Testando Dados Dinâmicos</h2>
<p>A maioria das aplicações reais busca dados de APIs, faz requisições HTTP ou aguarda estados que mudam com o tempo. Testing Library oferece ferramentas específicas para lidar com esses padrões assíncronos sem tornar seus testes frágeis e dependentes de timing.</p>
<h3>waitFor: Aguardando Condições</h3>
<p><code>waitFor</code> é a ferramenta fundamental para testes assíncronos. Ela executa uma função repetidamente até que a condição seja verdadeira ou timeout expire. A diferença crítica: waitFor aguarda uma condição lógica, não apenas a presença de um elemento.</p>
<pre><code class="language-jsx"></code></pre>
<h3>findBy: O Atalho Assíncrono</h3>
<p><code>findBy</code> é basicamente <code>waitFor</code> + <code>getBy</code>. Ela retorna uma promise que resolve quando o elemento aparece. É mais concisa para casos simples onde você apenas quer encontrar um elemento que aparecerá depois.</p>
<pre><code class="language-jsx"></code></pre>
<h3>Mockando APIs com MSW (Mock Service Worker)</h3>
<p>Para testes realmente confiáveis, mocking da API é essencial. Mock Service Worker (MSW) intercepta requisições HTTP em nível de rede, permitindo testes realistas sem tocar em servidores reais.</p>
<pre><code class="language-jsx">import { render, screen, waitFor } from '@testing-library/react';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
const server = setupServer(
http.get('/api/users/:userId', () => {
return HttpResponse.json({ name: 'Maria Santos', email: 'maria@example.com' });
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
function UserCard({ userId }) {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
fetch(/api/users/${userId})
.then(r => r.json())
.then(setUser);
}, [userId]);
return user ? (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
) : null;
}
test('renderiza card de usuário com dados da API', async () => {
render(<UserCard userId="123" />);
await waitFor(() => {
expect(screen.getByText('Maria Santos')).toBeInTheDocument();
});
expect(screen.getByText('maria@example.com')).toBeInTheDocument();
});
test('trata erros de API corretamente', async () => {
server.use(
http.get('/api/users/:userId', () => {
return HttpResponse.json(
{ error: 'Not found' },
{ status: 404 }
);
})
);
render(<UserCard userId="999" />);
// Component não renderiza nada em caso de erro
expect(screen.queryByRole('heading')).not.toBeInTheDocument();
});</code></pre>
<h3>Testando Loading States e Transições</h3>
<p>Um padrão comum é testar que o componente mostra loading antes de mostrar dados. Para isso, use <code>screen.queryByText</code> para verificar presença antes, e <code>waitFor</code> para verificar depois.</p>
<pre><code class="language-jsx">function DataTable({ endpoint }) {
const [data, setData] = React.useState([]);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
fetch(endpoint)
.then(r => r.json())
.then(items => {
setData(items);
setLoading(false);
});
}, [endpoint]);
return (
<div>
{loading && <p role="status">Carregando dados...</p>}
{!loading && data.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
test('transição de loading para dados', async () => {
render(<DataTable endpoint="/api/items" />);
// Inicialmente mostra loading
expect(screen.getByRole('status')).toHaveTextContent('Carregando dados...');
// Aguarda loading desaparecer e dados aparecerem
await waitFor(() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});
// Agora devem aparecer os itens
await screen.findByText('Item 1');
});</code></pre>
<h2>Boas Práticas e Padrões Avançados</h2>
<h3>Configurando um Setup Completo</h3>
<p>Em projetos reais, você quer um setup reutilizável. Crie um arquivo de configuração que outras suites de testes herdam.</p>
<pre><code class="language-jsx">// test-utils.jsx
import React from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
const renderWithUser = (component) => {
return {
user: userEvent.setup(),
...render(component),
};
};
export * from '@testing-library/react';
export { renderWithUser };
// Em seus testes
import { screen, renderWithUser } from './test-utils';
test('exemplo usando o wrapper customizado', async () => {
const { user } = renderWithUser(<MyComponent />);
await user.click(screen.getByRole('button'));
// ...
});</code></pre>
<h3>Testando Formulários Complexos</h3>
<p>Formulários com validação, campos dinâmicos e submissão assíncrona são comuns. O padrão é: renderizar, preencher, submeter, aguardar resultado.</p>
<pre><code class="language-jsx">function RegistrationForm({ onSubmit }) {
const [errors, setErrors] = React.useState({});
const [submitted, setSubmitted] = React.useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
const form = new FormData(e.target);
// Validação simples
const newErrors = {};
if (!form.get('email').includes('@')) newErrors.email = 'Email inválido';
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
await onSubmit(Object.fromEntries(form));
setSubmitted(true);
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" />
{errors.email && <span role="alert">{errors.email}</span>}
<button type="submit">Registrar</button>
{submitted && <p role="status">Registrado com sucesso!</p>}
</form>
);
}
test('validação de formulário de registro', async () => {
const handleSubmit = jest.fn();
const { user } = renderWithUser(
<RegistrationForm onSubmit={handleSubmit} />
);
// Tentar submeter com email inválido
await user.type(screen.getByLabelText('Email'), 'invalido');
await user.click(screen.getByRole('button', { name: /registrar/i }));
// Erro é exibido
expect(screen.getByRole('alert')).toHaveTextContent('Email inválido');
expect(handleSubmit).not.toHaveBeenCalled();
// Limpar e tentar novamente com email válido
const input = screen.getByLabelText('Email');
await user.clear(input);
await user.type(input, 'usuario@example.com');
await user.click(screen.getByRole('button', { name: /registrar/i }));
// Aguardar sucesso
await screen.findByRole('status', { name: /sucesso/i });
expect(handleSubmit).toHaveBeenCalledWith(
expect.objectContaining({ email: 'usuario@example.com' })
);
});</code></pre>
<h3>Evitando Armadilhas Comuns</h3>
<p>A armadilha mais comum é testar implementação em vez de comportamento. Outra é não usar <code>waitFor</code> quando necessário, causando testes que falham intermitentemente (flaky tests). Uma terceira é usar <code>setTimeout</code> em testes, o que é sempre um sinal de design ruim.</p>
<pre><code class="language-jsx"></code></pre>
<h2>Conclusão</h2>
<p>Três pilares transformam seus testes em confiáveis e resilientes:</p>
<ol>
<li><strong>Queries semanticamente corretas</strong> (<code>getByRole</code> preferencialmente) garantem que você está testando a mesma forma como usuários reais interagem, e como consequência, força acessibilidade no código.</li>
</ol>
<ol>
<li><strong>userEvent em vez de fireEvent</strong> simula comportamento real do navegador, detectando bugs que fireEvent simples não revelaria, como eventos faltantes ou sequência incorreta.</li>
</ol>
<ol>
<li><strong>Padrões assíncronos apropriados</strong> (<code>waitFor</code>, <code>findBy</code>, MSW) permitem testar dados dinâmicos e requisições sem fragilidade, tornando seus testes verdes consistentemente.</li>
</ol>
<p>Testing Library não é apenas uma biblioteca, é uma filosofia. Quando você escreve testes que focam no comportamento do usuário, seu código fica melhor, mais acessível e mais fácil de refatorar. Comece com essas práticas agora e evitará a maioria dos problemas que vejo em codebases reais.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://testing-library.com/" target="_blank" rel="noopener noreferrer">Testing Library Documentation</a></li>
<li><a href="https://testing-library.com/docs/user-event/intro" target="_blank" rel="noopener noreferrer">User Event Documentation</a></li>
<li><a href="https://mswjs.io/" target="_blank" rel="noopener noreferrer">Mock Service Worker (MSW)</a></li>
<li><a href="https://testingjavascript.com/" target="_blank" rel="noopener noreferrer">Kent C. Dodds - Testing JavaScript</a></li>
<li><a href="https://kentcdodds.com/blog/common-mistakes-with-react-testing-library" target="_blank" rel="noopener noreferrer">Common mistakes with React Testing Library</a></li>
</ul>
<p><!-- FIM --></p>