Skip to content
Giovani

Erros comuns com React Testing Library [Tradução]

react, tradução, react testing library8 min read

Artigo original (em Inglês) escrito por Kent C. Dodds

Olá 👋 Eu criei a biblioteca React Testing porque eu não estava satisfeito com o cenário de testes naquele momento. Ela se expandiu para a DOM Testing Library e agora nós temos implementações da Testing Library (wrappers) e ferramenta de teste que focam no DOM (e até algumas que não) para todos frameworks JavaScript populares.

Com o passar do tempo, nós fizemos algumas pequenas mudanças na API e descobrimos alguns padrões subótimos. Apesar dos nossos esforços para documentar a "melhor forma" de usar as ferramentas que nós fornecemos, eu ainda vejo blog posts e testes escritos seguindo esses padrões subótimos e eu gostaria de passar por cada um deles, comentar o porquê de não serem ótimos e como você pode melhorar seus testes para evitar essas armadilhas.

Nota: Eu clasifiquei cada um deles pela importância:

  • baixa: isso é majoritariamente opinião minha, sinta-se a vontade para ignorar e você provavelmente não vai ter problemas
  • média: talvez você encontre alguns bugs, perca confiança, ou tenha trabalho extra desnecessariamente
  • alta: escute esse conselho! Você provavelmente está perdendo confiança e/ou terá testes problemáticos

Não usar os plugins para ESLint da Testing Library

Importância: média

Se você quer evitar vários destes erros comuns, então os plugins oficiais para ESLint podem te ajudar bastante:

Conselho: Instale e use os plugins para ESLint da Testing Library


Usar wrapper como nome da variável que recebe o retorno de render

Importância: baixa

1// ❌
2const wrapper = render(<Example prop="1" />)
3wrapper.rerender(<Example prop="2" />)
4
5// ✅
6const {rerender} = render(<Example prop="1" />)
7rerender(<Example prop="2" />)

O nome wrapper é coisa antiga herdada do enzyme e nós não precisamos disso aqui. O valor retornado de render não está "wrapping" nada. É simplesmente uma coleção de utilidades que (graças ao próximo item) você realmente não deveria precisar (com frequência) de qualquer forma.

Conselho: desestruture o que você precisar do retorno do render ou chame de view.


Usar cleanup

Importância: média

1// ❌
2import {render, screen, cleanup} from '@testing-library/react'
3
4afterEach(cleanup)
5
6// ✅
7import {render, screen} from '@testing-library/react'

Já faz bastante tempo que cleanup acontece automaticamente (com suporte na maioria dos grandes frameworks de teste) e você não precisa se preocupar com isso (saiba mais).

Conselho: não use cleanup


Não usar screen

Importância: média

1// ❌
2const {getByRole} = render(<Example />)
3const errorMessageNode = getByRole('alert')
4
5// ✅
6render(<Example />)
7const errorMessageNode = screen.getByRole('alert')

screen foi adicionado na versão v6.11.0 da DOM Testing Library (o que significa que você deveria ter acesso a ela em qualquer ambiente com @testing-library/react@>=9). Ela vem do mesmo import que você recebe render:

1import {render, screen} from '@testing-library/react'

O benefício de utilizar screen é que você não precisa ficar mudando o que você recebe do render conforme queira adicionar/remover as queries que você precisa. Você só precisa digitar screen. e deixar a mágica do autocomplete do seu editor cuidar do resto.

A única exceção para isso é se você está usando o container ou baseElement que você provavelmente deveria evitar (eu honestamente não consigo mais pensar em um caso de uso legítimo para essas opções e elas só existem por razões históricas neste momento)

Você também pode usar screen.debug ao invés de debug

Conselho: use screen para fazer as queries e usar o debug.


Usar a asserção (assertion) errada

Importância: alta

1const button = screen.getByRole('button', {name: /disabled button/i})
2
3// ❌
4expect(button.disabled).toBe(true)
5// Mensagem de erro:
6// expect(received).toBe(expected) // Object.is equality
7//
8// Expected: true
9// Received: false
10
11// ✅
12expect(button).toBeDisabled()
13// Mensagem de erro:
14// Received element is not disabled:
15// <button />

Aquela asserção toBeDisabled vem da jest-dom. É fortemente recomendado o uso da jest-dom pois as mensagens de erro são muito melhores.

Conselho: instale e use @testing-library/jest-dom


Colocar act em volta das coisas desnecessariamente

Importância: média

1// ❌
2act(() => {
3 render(<Example />)
4})
5
6const input = screen.getByRole('textbox', {name: /choose a fruit/i})
7act(() => {
8 fireEvent.keyDown(input, {key: 'ArrowDown'})
9})
10
11// ✅
12render(<Example />)
13const input = screen.getByRole('textbox', {name: /choose a fruit/i})
14fireEvent.keyDown(input, {key: 'ArrowDown'})

Eu vejo pessoas colocando act em volta de coisas como essas porque elas veem esses warnings com act o tempo todo e estão apenas tentando desesperadamente fazer com que eles desapareçam, mas o que elas não sabem é que render e fireEvent já estão envolvidos por act! Então aqueles ali que elas colocam em volta não está fazendo nada útil.

Na maioria das vezes, se você está vendo um warning com act, não é algo pra só ignorar ou tentar fazer desaparecer, na verdade isso está te avisando que algo inesperado está acontecendo com seu teste. Você pode aprender mais sobre isso com o meu blog post (e vídeos): Corrija o warning "not wrapped in act(...)".

Conselho: Aprenda quando act é necessário e não coloque act em volta das coisas desnecessariamente.


Usar a query errada

Importância: alta

1// ❌
2// Assumindo que você está trabalhando em cima desse DOM:
3// <label>Username</label><input data-testid="username" />
4screen.getByTestId('username')
5
6// ✅
7// Mude o DOM para ser acessível associando a label ao input e informando o tipo do input
8// <label for="username">Username</label><input id="username" type="text" />
9screen.getByRole('textbox', {name: /username/i})

Nós mantemos uma página chamada "Which query should I use?" (Qual query eu deveria usar?) de queries que você deveria tentar usar na ordem que você deveria tentar usar. Se o seu objetivo está alinhado com o nosso que é ter testes que tragam confiança que sua aplicação vai funcionar quando seus usuários a usarem, então você vai querer aplicar as queries no DOM o mais próximo possível da forma que o seu usuário final faz (indiretamente, mas faz). As queries que nós fornecemos te ajudam a fazer isso, mas nem todas as queries são criadas igualmente.

Usar container para fazer query por elementos

Como uma subseção de "Usar a query errada" Eu quero falar sobre usando a query diretamente no container.

1// ❌
2const {container} = render(<Example />)
3const button = container.querySelector('.btn-primary')
4expect(button).toHaveTextContent(/click me/i)
5
6// ✅
7render(<Example />)
8screen.getByRole('button', {name: /click me/i})

Nós queremos garantir que seus usuários possam interagir com a sua UI e se você fizer as queries por aí usando querySelector nós perdemos grande parte dessa confiança, o teste é mais difícil de ler, e vai quebrar com mais frequência. Isso está de mãos dadas com a próxima subseção:

Não fazer a query por texto

Como uma subseção de "Usar a query errada", eu quero falar porque eu recomendo que você use a query pelo texto verdadeiro (Em caso de localização, eu recomendo que use o padrão), ao invés ficar usando test IDs ou outros mecanismos em todo canto.

1// ❌
2screen.getByTestId('submit-button')
3
4// ✅
5screen.getByRole('button', {name: /submit/i})

Se você não usa a query com o texto verdadeiro, então você tem que fazer trabalho extra para ter certeza que suas traduções estão sendo aplicadas corretamente. A maior reclamação que eu escuto sobre isso é que faz com que criadores de conteúdo quebrem seus testes. Minha réplica a isso é que primeiro, se um criador de conteúdo troca "Username" por "Email", essa é uma troca que eu definitivamente quero saber sobre (porque eu vou ter que mudar minha implementação). Além disso, se há uma situação que eles quebram algo, arrumar isso não toma tanto tempo assim. É fácil de identificar e de corrigir.

Então o custo é bem baixo, e o benefício é que você aumenta a confiança de que suas traduções estão sendo aplicadas corretamente e seus testes são mais fáceis de escrever e de ler.

Vale mencionar que nem todo mundo concorda comigo nisso, fica a vontade para ler mais sobre isso nesse fio no twitter.

Não usar *ByRole na maior parte do tempo

Como uma subseção de "Usar a query errada" eu quero falar sobre *ByRole. Nas versões recentes, as queries *ByRole foram seriamente melhoradas (primeiramente graças ao grande trabalho do Sebastian Silbermann) e agora são as mais recomendadas para o uso nos testes dos seus componentes. Aqui estão algumas das minhas features favoritas desse tipo de query.

A opção name permite que você faça a query por elementos pelo seu "Nome acessível" que é o que leitores de tela irão ler ao ler o elemento e funciona mesmo que seu elemento tenha o texto dividido entre múltiplos elementos. Por exemplo:

1// Assumindo que nós temos essa estrutura de DOM para trabalhar
2// <button><span>Hello</span> <span>World</span></button>
3
4screen.getByText(/hello world/i)
5// ❌ Falha com o seguinte erro:
6// Unable to find an element with the text: /hello world/i. This could be
7// because the text is broken up by multiple elements. In this case, you can
8// provide a function for your text matcher to make your matcher more flexible.
9
10screen.getByRole('button', {name: /hello world/i})
11// ✅ funciona!

Uma razão pela qual as pessoas não usam as queries *ByRole é porque elas não estão familiarizadas com os roles implícitos colocado nos elementos. Aqui está uma lista de Roles no MDN. Outra que também é uma das minhas features favoritas das queries *ByRole é que se não conseguirmos encontrar um elemento com o role que você específicou, nós vamos mostrar na tela não somente o DOM inteiro como em caso de erros quando usadas as variantes de get* ou find*, mas também iremos mostrar todos os roles disponíveis para você poder usar na query.

1// Assumindo que esse é o DOM que nós temos para trabalhar
2// <button><span>Hello</span> <span>World</span></button>
3screen.getByRole('blah')

Isso vai falhar com a seguinte mensagem de erro:

1TestingLibraryElementError: Unable to find an accessible element with the role "blah"
2
3Here are the accessible roles:
4
5 button:
6
7 Name "Hello World":
8 <button />
9
10 --------------------------------------------------
11
12<body>
13 <div>
14 <button>
15 <span>
16 Hello
17 </span>
18
19 <span>
20 World
21 </span>
22 </button>
23 </div>
24</body>

Note que nós não tivemos que adicionar role=button ao nosso botão para que ele tivesse o role button. Esse é um role implícito, o que nos leva perfeitamente ao nosso próximo item...

Conselho: Leia e siga as recomendações do Guia "Qual Query Eu Deveria Usar?".


Adicionar aria-, role, e outros atributos de acessibilidade incorretamente

Importância: alta

1// ❌
2render(<button role="button">Click me</button>)
3
4// ✅
5render(<button>Click me</button>)

Sair distribuindo atributos de acessibilidade sem pensar não só é desnecessário (como no caso acima), mas também pode confundir os leitores de tela e seus usuários. Os atributos de acessibilidade deveriam ser utilizados apenas quando o HTML semântico não é o bastante (como no caso de você estar desenvolvendo uma UI não-nativa que você quer tornar acessível como um autocomplete). Se é isso que você está desenvolvendo, use uma biblioteca existente que seja acessível ou siga as práticas indicadas para WAI-ARIA. Geralmente eles tem ótimos exemplos.

Obs: para fazer inputs acessíveis via "role" você terá que especificar o atributo type!

Conselho: Evite adicionar atributos de acessibilidade desnecessários ou incorretos.


Não usar @testing-library/user-event

Importância: média

1// ❌
2fireEvent.change(input, {target: {value: 'hello world'}})
3
4// ✅
5userEvent.type(input, 'hello world')

@testing-library/user-event é um pacote desenvolvido baseado no fireEvent, mas ele fornece vários métodos que se assemelham mais com as interações feitas pelo usuário. No exemplo acima, fireEvent.change vai simplesmente disparar um único evento change no input. Entretanto, o userEvent.type, além do change, vai disparar os eventos keyDown, keyPress e keyUp para cada um dos caracteres. É muito mais próximo da interação real do usuário. Isso tem o benefício de funcionar bem com bibliotecas que talvez você use que não reagem ao evento change e sim aos outros.

Nós ainda estamos trabalhando na @testing-library/user-event para garantir que entregue o que nós prometemos: disparar todos os mesmo eventos que o usuário dispararia ao fazer uma ação específica. Eu não acho que já chegamos lá e é por isso que ela não vem por padrão com o @testing-library/dom (mas pode ser que venha em algum momento no futuro). Apesar disso, eu estou confiante o bastante para recomendar que você dê uma olhada e use as utilidades que ela fornece ao invés de fireEvent.

Conselho: Use @testing-library/user-event ao invés de fireEvent onde for possível.


Usar as variantes query* para qualquer coisa diferente de testar que coisas não existem

Importância: alta

1// ❌
2expect(screen.queryByRole('alert')).toBeInTheDocument()
3
4// ✅
5expect(screen.getByRole('alert')).toBeInTheDocument()
6expect(screen.queryByRole('alert')).not.toBeInTheDocument()

A única razão que as queries do tipo query* são fornecidas é para você ter uma função que pode chamar sem estourar um erro se nenhum elemento é encontrado (elas retornam null quando isso acontece). A única razão para qual isso é útil é para verificar que um elemento não foi renderizado na página.

Conselho: Use as variantes query* somente quando seu teste quer garantir que um elemento não pode ser encontrado.


Usar waitFor para esperar por elementos que podem ser encontrados usando a query find*

Importância: alta

1// ❌
2const submitButton = await waitFor(() =>
3 screen.getByRole('button', {name: /submit/i}),
4)
5
6// ✅
7const submitButton = await screen.findByRole('button', {name: /submit/i})

Esses dois trechos de código são basicamente equivalentes (as queries find* usam waitFor por debaixo dos panos), mas a segunda é mais simples e tem uma mensagem de erro melhor.

Conselho: use find* sempre que você queira fazer uma query por algo que pode não estar disponível naquele momento.


Passar uma função de callback vazia para waitFor

Importância: alta

1// ❌
2await waitFor(() => {})
3expect(window.fetch).toHaveBeenCalledWith('foo')
4expect(window.fetch).toHaveBeenCalledTimes(1)
5
6// ✅
7await waitFor(() => expect(window.fetch).toHaveBeenCalledWith('foo'))
8expect(window.fetch).toHaveBeenCalledTimes(1)

O propósito de waitFor é permitir você esperar que algo específico aconteça. Se você passar uma função de callback vazia pode ser que funcione hoje porque tudo que você precisa esperar é por "um tick do event loop" graças a forma que seus mocks funcionam. Mas você vai ficar com um teste frágil que pode facilmente falhar caso você refatore a lógica do trecho assíncrono do seu código.

Conselho: Espere por uma asserção específica dentro do waitFor.


Ter múltiplas asserções dentro de um único callback passado para o waitFor

Importância: baixa

1// ❌
2await waitFor(() => {
3 expect(window.fetch).toHaveBeenCalledWith('foo')
4 expect(window.fetch).toHaveBeenCalledTimes(1)
5})
6
7// ✅
8await waitFor(() => expect(window.fetch).toHaveBeenCalledWith('foo'))
9expect(window.fetch).toHaveBeenCalledTimes(1)

Digamos que no exemplo acima, window.fetch foi chamado duas vezes. Então, o waitFor vai falhar, entretanto, nós vamos ter que esperar pelo timeout antes que vejamos a falha do teste. Colocando apenas uma asserção ali, nós podemos esperar ambos, que a UI chege ao estado sob o qual nós queremos fazer os testes, e também falhe mais rápido se alguma das asserções acabar falhando.

Conselho: coloque apenas uma asserção dentro do callback.


Causar efeitos colaterais (side-effects) dentro de waitFor

Importância: alta

1// ❌
2await waitFor(() => {
3 fireEvent.keyDown(input, {key: 'ArrowDown'})
4 expect(screen.getAllByRole('listitem')).toHaveLength(3)
5})
6
7// ✅
8fireEvent.keyDown(input, {key: 'ArrowDown'})
9await waitFor(() => {
10 expect(screen.getAllByRole('listitem')).toHaveLength(3)
11})

waitFor é indicado para coisas que vão levar uma quantidade de tempo indeterminada entre a ação que você executou e a asserção que você está esperando se tornar verdade. Por conta disso, o callback pode ser chamado (ou testado) um número indeterminado de vezes numa frequência também indeterminada (já que é chamado repetidas vezes com um intervalo e também sempre que o DOM sofre mutações). Então, isso significa que o seu efeito colateral pode estar sendo executado várias vezes!

Isso também significa que você não pode usar snapshots com waitFor. Se você quer usar o teste por snapshot, então primeiro use o waitFor para esperar por uma asserção específica, e depois você pode testar usando o snapshot.

Conselho: coloque efeitos colaterais (side-effects) fora da função de callback passada para o waitFor e reserve a função de callback apenas para a asserção.


Usar as variantes de get* como asserções

Importância: baixa

1// ❌
2screen.getByRole('alert', {name: /error/i})
3
4// ✅
5expect(screen.getByRole('alert', {name: /error/i})).toBeInTheDocument()

Esse não é grande coisa na verdade, mas eu pensei em mencioná-lo e dar minha opinião. Se as queries get* não tiverem sucesso em encontrar o elemento, elas vão disparar uma mensagem de error bem útil que mostra a estrutura completa do DOM (com syntax highlighting) que vai te ajudar a debugar. Por conta disso, é impossível que a asserção falhe (porque a query vai disparar o erro antes que a asserção tenha chance de falhar).

Por essa razão, muitas pessoas deixam sem a asserção. Honestamente, isso é ok, mas eu, pessoalmente, mantenho a asserção lá só para comunicar para os leitores do código que não é apenas uma query perdida depois de alguma refatoração e sim que eu estou explicitamente testando que aquilo existe.

Conselho: Se você quer testar se algo existe, faça a asserção explicitamente.


Conclusão

Como mantenedores da família de ferramentas Testing Library, damos o nosso melhor para fazer APIs que levam as pessoas a usarem as coisas da forma mais eficaz possível e onde isso falha, tentamos documentar corretamente. Mas isso pode ser bem difícil (especialmente conforme as APIs são alteradas/melhoradas/etc). Espero que isso tenha sido útil para vocês. Nós realmente só queremos que vocês tenham mais sucesso na entrega do seu software com confiança.

Boa Sorte!