React & Frontend

Boas Práticas de Testing Library em Profundidade: Queries, Fire Events e Async para Times Ágeis

16 min de leitura

Boas Práticas de Testing Library em Profundidade: Queries, Fire Events e Async para Times Ágeis

Testing Library em Profundidade: Queries, Fire Events e Async 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. 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. Queries: A Arte de Encontrar Elementos 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

<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 (

&lt;article&gt;

&lt;h2&gt;{name}&lt;/h2&gt;

&lt;button onClick={onAddToCart}&gt;Adicionar ao carrinho&lt;/button&gt;

&lt;/article&gt;

);

}

test(&#039;card de produto está semanticamente correto&#039;, () =&gt; {

const handleClick = jest.fn();

render(&lt;ProductCard name=&quot;Teclado&quot; onAddToCart={handleClick} /&gt;);

// getByRole encontra por semantic HTML

const heading = screen.getByRole(&#039;heading&#039;, { level: 2, name: /teclado/i });

expect(heading).toBeInTheDocument();

const button = screen.getByRole(&#039;button&#039;, { 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 &#039;@testing-library/react&#039;;

import userEvent from &#039;@testing-library/user-event&#039;;

function TodoForm({ onAdd }) {

const [input, setInput] = React.useState(&#039;&#039;);

const handleSubmit = (e) =&gt; {

e.preventDefault();

if (input.trim()) {

onAdd(input);

setInput(&#039;&#039;);

}

};

return (

&lt;form onSubmit={handleSubmit}&gt;

&lt;input

type=&quot;text&quot;

value={input}

onChange={(e) =&gt; setInput(e.target.value)}

placeholder=&quot;Nova tarefa&quot;

/&gt;

&lt;button type=&quot;submit&quot;&gt;Adicionar&lt;/button&gt;

&lt;/form&gt;

);

}

test(&#039;usuário pode adicionar tarefa&#039;, async () =&gt; {

const handleAdd = jest.fn();

render(&lt;TodoForm onAdd={handleAdd} /&gt;);

const user = userEvent.setup();

const input = screen.getByPlaceholderText(&#039;Nova tarefa&#039;);

const button = screen.getByRole(&#039;button&#039;, { name: /adicionar/i });

// userEvent mantém estado entre eventos

await user.type(input, &#039;Estudar Testing Library&#039;);

await user.click(button);

expect(handleAdd).toHaveBeenCalledWith(&#039;Estudar Testing Library&#039;);

expect(input.value).toBe(&#039;&#039;); // 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 (

&lt;div&gt;

&lt;button onClick={() =&gt; setOpen(!open)}&gt;Menu&lt;/button&gt;

{open &amp;&amp; (

&lt;menu&gt;

&lt;li&gt;&lt;a href=&quot;/profile&quot;&gt;Perfil&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href=&quot;/settings&quot;&gt;Configurações&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href=&quot;/logout&quot;&gt;Sair&lt;/a&gt;&lt;/li&gt;

&lt;/menu&gt;

)}

&lt;/div&gt;

);

}

test(&#039;interações complexas com dropdown&#039;, async () =&gt; {

render(&lt;DropdownMenu /&gt;);

const user = userEvent.setup();

const button = screen.getByRole(&#039;button&#039;, { name: /menu/i });

// Menu não deve estar visível inicialmente

expect(screen.queryByRole(&#039;menuitem&#039;)).not.toBeInTheDocument();

// Click abre o menu

await user.click(button);

expect(screen.getAllByRole(&#039;link&#039;)).toHaveLength(3);

// Click novamente fecha

await user.click(button);

expect(screen.queryByRole(&#039;menuitem&#039;)).not.toBeInTheDocument();

// Tab e Enter também funcionam

await user.click(button);

const profileLink = screen.getByRole(&#039;link&#039;, { name: /perfil/i });

await user.tab();

expect(document.activeElement).toBe(profileLink);

await user.keyboard(&#039;{Enter}&#039;);

});</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 &#039;@testing-library/react&#039;;

import { setupServer } from &#039;msw/node&#039;;

import { http, HttpResponse } from &#039;msw&#039;;

const server = setupServer(

http.get(&#039;/api/users/:userId&#039;, () =&gt; {

return HttpResponse.json({ name: &#039;Maria Santos&#039;, email: &#039;maria@example.com&#039; });

})

);

beforeAll(() =&gt; server.listen());

afterEach(() =&gt; server.resetHandlers());

afterAll(() =&gt; server.close());

function UserCard({ userId }) {

const [user, setUser] = React.useState(null);

React.useEffect(() =&gt; {

fetch(/api/users/${userId})

.then(r =&gt; r.json())

.then(setUser);

}, [userId]);

return user ? (

&lt;div&gt;

&lt;h2&gt;{user.name}&lt;/h2&gt;

&lt;p&gt;{user.email}&lt;/p&gt;

&lt;/div&gt;

) : null;

}

test(&#039;renderiza card de usuário com dados da API&#039;, async () =&gt; {

render(&lt;UserCard userId=&quot;123&quot; /&gt;);

await waitFor(() =&gt; {

expect(screen.getByText(&#039;Maria Santos&#039;)).toBeInTheDocument();

});

expect(screen.getByText(&#039;maria@example.com&#039;)).toBeInTheDocument();

});

test(&#039;trata erros de API corretamente&#039;, async () =&gt; {

server.use(

http.get(&#039;/api/users/:userId&#039;, () =&gt; {

return HttpResponse.json(

{ error: &#039;Not found&#039; },

{ status: 404 }

);

})

);

render(&lt;UserCard userId=&quot;999&quot; /&gt;);

// Component não renderiza nada em caso de erro

expect(screen.queryByRole(&#039;heading&#039;)).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(() =&gt; {

fetch(endpoint)

.then(r =&gt; r.json())

.then(items =&gt; {

setData(items);

setLoading(false);

});

}, [endpoint]);

return (

&lt;div&gt;

{loading &amp;&amp; &lt;p role=&quot;status&quot;&gt;Carregando dados...&lt;/p&gt;}

{!loading &amp;&amp; data.map(item =&gt; (

&lt;div key={item.id}&gt;{item.name}&lt;/div&gt;

))}

&lt;/div&gt;

);

}

test(&#039;transição de loading para dados&#039;, async () =&gt; {

render(&lt;DataTable endpoint=&quot;/api/items&quot; /&gt;);

// Inicialmente mostra loading

expect(screen.getByRole(&#039;status&#039;)).toHaveTextContent(&#039;Carregando dados...&#039;);

// Aguarda loading desaparecer e dados aparecerem

await waitFor(() =&gt; {

expect(screen.queryByRole(&#039;status&#039;)).not.toBeInTheDocument();

});

// Agora devem aparecer os itens

await screen.findByText(&#039;Item 1&#039;);

});</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 &#039;react&#039;;

import { render } from &#039;@testing-library/react&#039;;

import userEvent from &#039;@testing-library/user-event&#039;;

const renderWithUser = (component) =&gt; {

return {

user: userEvent.setup(),

...render(component),

};

};

export * from &#039;@testing-library/react&#039;;

export { renderWithUser };

// Em seus testes

import { screen, renderWithUser } from &#039;./test-utils&#039;;

test(&#039;exemplo usando o wrapper customizado&#039;, async () =&gt; {

const { user } = renderWithUser(&lt;MyComponent /&gt;);

await user.click(screen.getByRole(&#039;button&#039;));

// ...

});</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) =&gt; {

e.preventDefault();

const form = new FormData(e.target);

// Validação simples

const newErrors = {};

if (!form.get(&#039;email&#039;).includes(&#039;@&#039;)) newErrors.email = &#039;Email inválido&#039;;

if (Object.keys(newErrors).length &gt; 0) {

setErrors(newErrors);

return;

}

await onSubmit(Object.fromEntries(form));

setSubmitted(true);

};

return (

&lt;form onSubmit={handleSubmit}&gt;

&lt;label htmlFor=&quot;email&quot;&gt;Email&lt;/label&gt;

&lt;input id=&quot;email&quot; name=&quot;email&quot; type=&quot;email&quot; /&gt;

{errors.email &amp;&amp; &lt;span role=&quot;alert&quot;&gt;{errors.email}&lt;/span&gt;}

&lt;button type=&quot;submit&quot;&gt;Registrar&lt;/button&gt;

{submitted &amp;&amp; &lt;p role=&quot;status&quot;&gt;Registrado com sucesso!&lt;/p&gt;}

&lt;/form&gt;

);

}

test(&#039;validação de formulário de registro&#039;, async () =&gt; {

const handleSubmit = jest.fn();

const { user } = renderWithUser(

&lt;RegistrationForm onSubmit={handleSubmit} /&gt;

);

// Tentar submeter com email inválido

await user.type(screen.getByLabelText(&#039;Email&#039;), &#039;invalido&#039;);

await user.click(screen.getByRole(&#039;button&#039;, { name: /registrar/i }));

// Erro é exibido

expect(screen.getByRole(&#039;alert&#039;)).toHaveTextContent(&#039;Email inválido&#039;);

expect(handleSubmit).not.toHaveBeenCalled();

// Limpar e tentar novamente com email válido

const input = screen.getByLabelText(&#039;Email&#039;);

await user.clear(input);

await user.type(input, &#039;usuario@example.com&#039;);

await user.click(screen.getByRole(&#039;button&#039;, { name: /registrar/i }));

// Aguardar sucesso

await screen.findByRole(&#039;status&#039;, { name: /sucesso/i });

expect(handleSubmit).toHaveBeenCalledWith(

expect.objectContaining({ email: &#039;usuario@example.com&#039; })

);

});</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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

Guia Completo de React Query Avançado: Cache, Stale Time, Prefetch e Optimistic UI
Guia Completo de React Query Avançado: Cache, Stale Time, Prefetch e Optimistic UI

Entendendo o Cache e sua Importância no React Query O cache é o coração do Re...

Headless Components em React: Lógica sem Apresentação com Radix UI na Prática
Headless Components em React: Lógica sem Apresentação com Radix UI na Prática

O Que São Headless Components? Um headless component é um componente React qu...

Controlled vs Uncontrolled Components: Quando Usar Cada Abordagem na Prática
Controlled vs Uncontrolled Components: Quando Usar Cada Abordagem na Prática

O Que São Componentes Controlados e Não Controlados? Antes de mais nada, prec...