Artigo original: How to Start Testing Your React Apps Using the React Testing Library and Jest

Testes são frequentemente vistos como um processo tedioso. É código extra que você tem que escrever e, em alguns casos, para ser honesto, não é necessário. Todo desenvolvedor, no entanto, deve saber pelo menos o básico de testes. Isso aumenta a confiança nos produtos que eles constroem e, na maioria das empresas, é um requisito.

No mundo do React, há uma incrível biblioteca chamada react-testing-library, que auxilia no teste mais eficiente de suas aplicações em React. Você a utiliza com o Jest.

Neste artigo, veremos 8 etapas simples para começar a testar suas aplicações em React como um especialista.

Pré-requisitos

Neste tutorial, pressupõe-se que você tenha, pelo menos, um entendimento básico do React. Vou focar apenas na parte de testes.

Para acompanhá-lo, você precisa clonar o projeto, executando no seu terminal:

  git clone https://github.com/ibrahima92/prep-react-testing-library-guide

Depois, execute:

  yarn

Ou, se estiver usando o npm:

npm install

É isso! Agora, vamos mergulhar em alguns princípios básicos.

Básico

Alguns pontos-chave serão amplamente utilizados neste artigo. Compreender o papel deles pode auxiliar na compreensão

it ou test: descreve o próprio teste. Recebe como parâmetros o nome do teste e uma função que contém os testes.

expect: a condição que o teste precisa satisfazer. Ela vai comparar o parâmetro recebido com um 'matcher'.

matcher: a função aplicada à condição esperada.

render: o método usado para renderizar um determinado componente

import React from 'react'
import {render} from '@testing-library/react'
import App from './App'
 
 it('should take a snapshot', () => {
    const { asFragment } = render(<App />)
    
    expect(asFragment(<App />)).toMatchSnapshot()
   })
});

Como você pode ver, descrevemos o teste com isso. Em seguida, usamos o render para exibir o componente e esperamos que asFragment(<App />) corresponda a toMatchSnapshot() (o verificador fornecido pelo jest-dom).

A propósito, o método render retorna vários métodos que podemos usar para testar nossos recursos. Também usamos a desestruturação para obter o método.

Dito isso, vamos avançar e aprender mais sobre a React Testing Library na próxima seção.

O que é a React Testing Library?

A React Testing Library é um pacote muito leve e excelente criado por Kent C. Dodds. É uma substituição para o Enzyme e fornece funções utilitárias leves em cima do `react-dom`  e `react-dom/test-utils`.

O React Testing Library é uma biblioteca de teste de DOM, o que significa que, em vez de lidar com instâncias de componentes do React renderizados, ele lida com elementos do DOM e com o modo como eles se comportam diante de usuários reais.

É uma ótima biblioteca, relativamente fácil de começar a usar, e promove boas práticas de teste. Observação: você também pode utilizá-la sem o Jest.

"Quanto mais seus testes se assemelharem à maneira como seu software é utilizado, maior confiança eles podem lhe proporcionar".

Agora, vamos começar a utilizá-los na próxima seção. Aliás, você não precisa instalar nenhum pacote, já que o create-react-app vem com a biblioteca e suas dependências.

1. Como criar um snapshot de teste

Um snapshot (em português, algo como uma fotografia ou instantâneo), como o nome sugere, nos permite salvar uma imagem do estado atual de um componente específico. Isso é especialmente útil quando você faz atualizações ou realiza alguma refatoração e deseja obter ou comparar as mudanças.

Agora, vamos tirar um snapshot do arquivo App.js .

  • App.test.js
import React from 'react'
import {render, cleanup} from '@testing-library/react'
import App from './App'

 afterEach(cleanup)
 
 it('should take a snapshot', () => {
    const { asFragment } = render(<App />)
    
    expect(asFragment(<App />)).toMatchSnapshot()
   })
});

Para tirar um snapshot, primeiro precisamos importar os métodos render e cleanup. Esses dois métodos serão amplamente utilizados ao longo deste artigo

render, como você pode imaginar, é usado para renderizar um componente do React. A função cleanup é passada como um parâmetro para o afterEach para limpar tudo após cada teste, a fim de evitar vazamentos de memória.

Agora, podemos renderizar o componente App com o método render e obter asFragment como o valor retornado. Por fim, nos certificamos de que o fragmento do componente corresponda ao snapshot.

Agora, para executar o teste, abra o termina, navegue até a raiz do projeto e execute o seguinte comando:

  yarn test

Ou, se estiver usando npm:

  npm test

Como resultado, ele criará uma outra pasta __snapshots__ e um arquivo App.test.js.snap na pasta src, que se parecerá com isto:

  • App.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Take a snapshot should take a snapshot 1`] = `
<DocumentFragment>
  <div class="App">
    <h1>Testing</h1>
  </div>
</DocumentFragment>
`;

Se você fizer outra alteração em App.js, o teste falhará, pois o snapshot não corresponderá mais à condição. Para fazê-lo passar, basta pressionar 'u' para atualizá-lo. Você terá o snapshot atualizado no arquivo App.test.js.snap.

Agora, vamos prosseguir e começar a testar nossos elementos.

2. Testando elementos do DOM

Para testar nossos elementos do DOM, primeiro devemos examinar o arquivo TestElements.js

  • TestElements.js
import React from 'react'

const TestElements = () => {
 const [counter, setCounter] = React.useState(0)
  
 return (
  <>
    <h1 data-testid="counter">{ counter }</h1>
    <button data-testid="button-up" onClick={() => setCounter(counter + 1)}> Up</button>
    <button disabled data-testid="button-down" onClick={() => setCounter(counter - 1)}>Down</button>
 </>
    )
  }
  
export default TestElements

Aqui, a única coisa que você precisa manter é o data-testid. Ele será usado para selecionar esses elementos no arquivo de teste. Agora, vamos escrever nosso teste unitário.

Teste se o contador está igual a 0:

TestElements.test.js

import React from 'react';
import { render, cleanup } from '@testing-library/react';
import TestElements from './TestElements'

afterEach(cleanup);

  it('should equal to 0', () => {
    const { getByTestId } = render(<TestElements />); 
    expect(getByTestId('counter')).toHaveTextContent(0)
   });

Como você pode notar, a sintaxe é bastante similar ao teste anterior. A única diferença é que usamos getByTestId para selecionar os elementos necessários (lembre-se de data-testid) e checamos se isso passava no teste. Em outras palavras, verificamos se o conteúdo do texto <h1 data-testid="counter">{ counter }</h1> é igual a zero.

Teste se os botões estão habilitados ou desabilitados:

TestElements.test.js (adicione o seguinte bloco de código no arquivo)

   it('should be enabled', () => {
    const { getByTestId } = render(<TestElements />);
    expect(getByTestId('button-up')).not.toHaveAttribute('disabled')
  });

  it('should be disabled', () => {
    const { getByTestId } = render(<TestElements />); 
    expect(getByTestId('button-down')).toBeDisabled()
  });

Aqui, como de costume, usamos getByTestId para selecionar os elementos e verificar no primeiro teste se o botão possui o atributo 'disabled'. No segundo teste, verificamos se o botão está desabilitado ou não.

Se você salvar o arquivo ou rodar novamente no seu terminal o comando 'yarn test', os testes passarão.

Parabéns! Seu primeiro teste passou!

source

Agora, vamos aprender como testar um evento na próxima seção.

3. Testando eventos

Antes de escrever nossos testes unitários, vamos primeiro verificar como é o arquivo TestEvents.js

  • TestEvents.js
import React from 'react'

const TestEvents = () => {
  const [counter, setCounter] = React.useState(0)
  
return (
  <>
    <h1 data-testid="counter">{ counter }</h1>
    <button data-testid="button-up" onClick={() => setCounter(counter + 1)}> Up</button>
    <button data-testid="button-down" onClick={() => setCounter(counter - 1)}>Down</button>
 </>
    )
  }
  
  export default TestEvents

Agora, vamos escrever os testes.

Teste se o contador incrementa e decrementa corretamente quando clicamos nos botões

TestEvents.test.js

import React from 'react';
import { render, cleanup, fireEvent } from '@testing-library/react';
import TestEvents from './TestEvents'

  afterEach(cleanup);
  
  it('increments counter', () => {
    const { getByTestId } = render(<TestEvents />); 
    
    fireEvent.click(getByTestId('button-up'))

    expect(getByTestId('counter')).toHaveTextContent('1')
  });

  it('decrements counter', () => {
    const { getByTestId } = render(<TestEvents />); 
    
    fireEvent.click(getByTestId('button-down'))

    expect(getByTestId('counter')).toHaveTextContent('-1')
  });

Como você pode ver, esses dois testes são muito semelhantes, exceto pelo conteúdo de texto esperado. O primeiro teste dispara um evento de clique com fireEvent.click() para verificar se o contador incrementa para 1 quando o botão é clicado.

O segundo teste verifica se o contador decrementa para -1 quando o botão é clicado.

fireEvent tem vários métodos que você pode usar para testar eventos. Fique à vontade para mergulhar na documentação e aprender mais.

Agora que sabemos como testar eventos, vamos avançar e aprender na próxima seção como lidar com ações assíncronas.

4. Testando ações assíncronas

Uma ação assíncrona é algo que pode levar tempo para ser concluído. Pode ser uma solicitação HTTP, um temporizador e assim por diante.

Agora, vamos verificar o arquivo TestAsync.js.

  • TestAsync.js
import React from 'react'

const TestAsync = () => {
  const [counter, setCounter] = React.useState(0)

  const delayCount = () => (
    setTimeout(() => {
      setCounter(counter + 1)
    }, 500)
  )
  
return (
  <>
    <h1 data-testid="counter">{ counter }</h1>
    <button data-testid="button-up" onClick={delayCount}> Up</button>
    <button data-testid="button-down" onClick={() => setCounter(counter - 1)}>Down</button>
 </>
    )
  }
  
  export default TestAsync

Aqui, usamos setTimeout() para atrasar o evento de incremento em 0,5 segundo.

Teste se o contador é incrementado após 0,5 segundo: TestAsync.test.js

import React from 'react';
import { render, cleanup, fireEvent, waitForElement } from '@testing-library/react';
import TestAsync from './TestAsync'

afterEach(cleanup);
  
  it('increments counter after 0.5s', async () => {
    const { getByTestId, getByText } = render(<TestAsync />); 

    fireEvent.click(getByTestId('button-up'))

    const counter = await waitForElement(() => getByText('1')) 

    expect(counter).toHaveTextContent('1')
  });


Para testar o evento de incremento, primeiro precisamos usar async/await para lidar com a ação, porque, como mencionei anteriormente, ela leva tempo para ser concluída.

Em seguida, usamos um novo método auxiliar getByText(). Ele é semelhante ao getByTestId(), exceto pelo fato de que getByText() seleciona o conteúdo do texto em vez do id ou data-testid.

Agora, após clicar no botão, esperamos que o contador seja incrementado com waitForElement(() => getByText('1')). Uma vez que o contador foi incrementado para 1, podemos passar para a condição e verificar se o contador é efetivamente igual a 1. Dito isso, vamos agora passar para casos de teste mais complexos.

Você está pronto?

source--1-

5. Testando o React Redux

Se você é novo no React Redux, este artigo (texto em inglês) pode ajudar você. Caso contrário, vamos verificar como o TestRedux.js se parece.

  • TestRedux.js
import React from 'react'
import { connect } from 'react-redux'

const TestRedux = ({counter, dispatch}) => {

 const increment = () => dispatch({ type: 'INCREMENT' })
 const decrement = () => dispatch({ type: 'DECREMENT' })
  
 return (
  <>
    <h1 data-testid="counter">{ counter }</h1>
    <button data-testid="button-up" onClick={increment}>Up</button>
    <button data-testid="button-down" onClick={decrement}>Down</button>
 </>
    )
  }
  
export default connect(state => ({ counter: state.count }))(TestRedux)

Agora, para o Redux:

  • store/reducer.js
export const initialState = {
    count: 0,
  }
  
  export function reducer(state = initialState, action) {
    switch (action.type) {
      case 'INCREMENT':
        return {
          count: state.count + 1,
        }
      case 'DECREMENT':
        return {
          count: state.count - 1,
        }
      default:
        return state
    }
  }

Como você pode ver, não há nada extravagante – é apenas um componente Counter básico manipulado pelo React Redux.

Agora, vamos escrever os testes unitários.

Teste se o estado inicial é igual a 0:

TestRedux.test.js

import React from 'react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { render, cleanup, fireEvent } from '@testing-library/react';
import { initialState, reducer } from '../store/reducer'
import TestRedux from './TestRedux'

const renderWithRedux = (
  component,
  { initialState, store = createStore(reducer, initialState) } = {}
) => {
  return {
    ...render(<Provider store={store}>{component}</Provider>),
    store,
  }
}

 afterEach(cleanup);

it('checks initial state is equal to 0', () => {
    const { getByTestId } = renderWithRedux(<TestRedux />)
    expect(getByTestId('counter')).toHaveTextContent('0')
  })

Existem algumas coisas que precisamos importar para testar o React Redux. Aqui, criamos nossa própria função auxiliar renderWithRedux() para renderizar o componente, já que ele será usado várias vezes.

renderWithRedux() recebe como parâmetros o componente a ser renderizado, o estado inicial e a loja.

Se não houver loja, ele criará uma. Se não receber um estado inicial ou uma loja, ele retornará um objeto vazio.

Em seguida, usamos render() para renderizar o componente e passamos a loja para o Provider. Dito isso, agora, podemos passar o componente TestRedux para renderWithRedux() para testar se o contador é igual a 0.

Teste se o contador incrementa e decrementa corretamente: TestRedux.test.js (adicione o seguinte bloco de código ao arquivo)

it('increments the counter through redux', () => {
  const { getByTestId } = renderWithRedux(<TestRedux />, 
    {initialState: {count: 5}
})
  fireEvent.click(getByTestId('button-up'))
  expect(getByTestId('counter')).toHaveTextContent('6')
})

it('decrements the counter through redux', () => {
  const { getByTestId} = renderWithRedux(<TestRedux />, {
    initialState: { count: 100 },
  })
  fireEvent.click(getByTestId('button-down'))
  expect(getByTestId('counter')).toHaveTextContent('99')
})


Para testar os eventos de incremento e decremento, passamos um estado inicial como segundo argumento para renderWithRedux().

Agora, podemos clicar nos botões e testar se o resultado esperado corresponde à condição ou não.

Vamos passar para a próxima seção e introduzir o React Context. React Router e Axios virão em seguida – você ainda está comigo?

source--2-

6. Testando o React Context

Se você é novo no React Context, confira este artigo primeiro (texto em inglês). Caso contrário, vamos verificar o arquivo TextContext.js.

  • TextContext.js
import React from "react"

export const CounterContext = React.createContext()

const CounterProvider = () => {
  const [counter, setCounter] = React.useState(0)
  const increment = () => setCounter(counter + 1)
  const decrement = () => setCounter(counter - 1)

  return (
    <CounterContext.Provider value={{ counter, increment, decrement }}>
      <Counter />
    </CounterContext.Provider>
  )
}

export const Counter = () => {  
    const { counter, increment, decrement } = React.useContext(CounterContext)   
    return (
     <>
       <h1 data-testid="counter">{ counter }</h1>
       <button data-testid="button-up" onClick={increment}> Up</button>
       <button data-testid="button-down" onClick={decrement}>Down</button>
    </>
       )
}

export default CounterProvider


Agora, o estado do contador é gerenciado por meio do React Context. Vamos escrever o teste unitário para verificar se ele se comporta conforme esperado.

Teste se o estado inicial é igual a 0:

TextContext.test.js

import React from 'react'
import { render, cleanup,  fireEvent } from '@testing-library/react'
import CounterProvider, { CounterContext, Counter } from './TestContext'

const renderWithContext = (
  component) => {
  return {
    ...render(
        <CounterProvider value={CounterContext}>
            {component}
        </CounterProvider>)
  }
}

afterEach(cleanup);

it('checks if initial state is equal to 0', () => {
    const { getByTestId } = renderWithContext(<Counter />)
    expect(getByTestId('counter')).toHaveTextContent('0')
})


Assim como na seção anterior com o React Redux, aqui usamos a mesma abordagem, criando uma função auxiliar renderWithContext() para renderizar o componente.

Dessa vez, porém, ela recebe apenas o componente como parâmetro. Para criar um outro contexto, passamos CounterContext para o Provider. Agora, podemos testar se o contador é inicialmente igual a 0 ou não.

Teste se o contador incrementa e decrementa corretamente: TextContext.test.js (adicione o seguinte bloco de código ao arquivo)

  it('increments the counter', () => {
    const { getByTestId } = renderWithContext(<Counter />)

    fireEvent.click(getByTestId('button-up'))
    expect(getByTestId('counter')).toHaveTextContent('1')
  })

  it('decrements the counter', () => {
    const { getByTestId} = renderWithContext(<Counter />)

    fireEvent.click(getByTestId('button-down'))
    expect(getByTestId('counter')).toHaveTextContent('-1')
  })

Como você pode ver, aqui disparamos um evento de clique para testar se o contador incrementa corretamente para 1 e decrementa para -1.

Dito isso, podemos passar para a próxima seção e introduzir o React Router.

7. Testando o React Router

Se você quiser se aprofundar no React Router, este artigo pode ajudar você (texto em inglês). Caso contrário, vamos verificar o arquivo TestRouter.js.

  • TestRouter.js
import React from 'react'
import { Link, Route, Switch,  useParams } from 'react-router-dom'

const About = () => <h1>About page</h1>

const Home = () => <h1>Home page</h1>

const Contact = () => {
  const { name } = useParams()
  return <h1 data-testid="contact-name">{name}</h1>
}

const TestRouter = () => {
    const name = 'John Doe'
    return (
    <>
    <nav data-testid="navbar">
      <Link data-testid="home-link" to="/">Home</Link>
      <Link data-testid="about-link" to="/about">About</Link>
      <Link data-testid="contact-link" to={`/contact/${name}`}>Contact</Link>
    </nav>
    
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
        <Route path="/about:name" component={Contact} />
      </Switch>
    </>
  )
}

export default TestRouter

Aqui, temos alguns componentes para renderizar ao navegar pela página inicial.

Agora, vamos escrever os testes:

  • TestRouter.test.js
import React from 'react'
import { Router } from 'react-router-dom'
import { render, fireEvent } from '@testing-library/react'
import { createMemoryHistory } from 'history'
import TestRouter from './TestRouter'


const renderWithRouter = (component) => {
    const history = createMemoryHistory()
    return { 
    ...render (
    <Router history={history}>
        {component}
    </Router>
    )
  }
}

it('should render the home page', () => {

  const { container, getByTestId } = renderWithRouter(<TestRouter />) 
  const navbar = getByTestId('navbar')
  const link = getByTestId('home-link')

  expect(container.innerHTML).toMatch('Home page')
  expect(navbar).toContainElement(link)
})


Para testar o React Router, primeiro, precisamos ter um histórico de navegação para começar.

Portanto, usamos createMemoryHistory() para, como o nome sugere, criar um histórico de navegação. Em seguida, usamos nossa função auxiliar renderWithRouter() para renderizar o componente e passamos o histórico para o componente Router. Com isso, podemos agora testar se a página carregada no início é a página inicial ou não e se a barra de navegação é carregada com os links esperados.

Teste se ele navega para outras páginas com os parâmetros quando clicamos nos links: TestRouter.test.js (adicione o seguinte bloco de código ao arquivo)

it('should navigate to the about page', ()=> {
  const { container, getByTestId } = renderWithRouter(<TestRouter />) 

  fireEvent.click(getByTestId('about-link'))

  expect(container.innerHTML).toMatch('About page')
})

it('should navigate to the contact page with the params', ()=> {
  const { container, getByTestId } = renderWithRouter(<TestRouter />) 
   
  fireEvent.click(getByTestId('contact-link'))
   
  expect(container.innerHTML).toMatch('John Doe')
})


Agora, para verificar se a navegação funciona, precisamos disparar um evento de clique nos links de navegação. Para o primeiro teste, verificamos se o conteúdo é igual ao texto na página Sobre (em inglês, about) e, para o segundo, testamos os parâmetros de roteamento e verificamos se foram passados corretamente.

Podemos passar para a seção final e aprender como testar uma solicitação com o Axios.

Estamos quase lá!

source--3-

8. Testando requisições HTTP

Como de costume, vamos primeiro ver como é o arquivo TextAxios.js.

  • TextAxios.js
import React from 'react'
import axios from 'axios'

const TestAxios = ({ url }) => {
  const [data, setData] = React.useState()

  const fetchData = async () => {
    const response = await axios.get(url)
    setData(response.data.greeting)    
 }     
 
 return (
  <>
    <button onClick={fetchData} data-testid="fetch-data">Load Data</button>
    { 
    data ?
    <div data-testid="show-data">{data}</div>:
    <h1 data-testid="loading">Loading...</h1>
    }
  </>
     )
}

export default TestAxios

Como você pode ver aqui, temos um componente simples que tem um botão para fazer uma solicitação.

Se os dados não estiverem disponíveis, ele exibirá uma mensagem de carregamento.

Agora, vamos escrever os testes. Teste se os dados são buscados e exibidos corretamente:

TextAxios.test.js

import React from 'react'
import { render, waitForElement, fireEvent } from '@testing-library/react'
import axiosMock from 'axios'
import TestAxios from './TestAxios'

jest.mock('axios')

it('should display a loading text', () => {

 const { getByTestId } = render(<TestAxios />)

  expect(getByTestId('loading')).toHaveTextContent('Loading...')
})

it('should load and display the data', async () => {
  const url = '/greeting'
  const { getByTestId } = render(<TestAxios url={url} />)

  axiosMock.get.mockResolvedValueOnce({
    data: { greeting: 'hello there' },
  })

  fireEvent.click(getByTestId('fetch-data'))

  const greetingData = await waitForElement(() => getByTestId('show-data'))

  expect(axiosMock.get).toHaveBeenCalledTimes(1)
  expect(axiosMock.get).toHaveBeenCalledWith(url)
  expect(greetingData).toHaveTextContent('hello there')
})

Esse caso de teste é um pouco diferente, pois lidamos com uma requisição HTTP e, para fazer isso, temos que simular uma solicitação do Axios com a ajuda de jest.mock('axios').

Agora, podemos usar axiosMock e aplicar um método get() a ele. Finalmente, usaremos a função mockResolvedValueOnce() do Jest para passar os dados simulados como parâmetro.

Com isso, para o segundo teste, podemos clicar no botão para buscar os dados e usar async/await para resolvê-los.

Temos que testar 3 coisas:

  1. Se a requisição HTTP foi feita corretamente;
  2. Se a requisição HTTP foi feita com o URL correto;
  3. Se os dados buscados correspondem à expectativa.

Para o primeiro teste, apenas verificamos se a mensagem de carregamento é exibida quando não temos dados para mostrar. Dito isso, terminamos com os 8 passos simples para começar a testar suas aplicações em React.

Não tenha mais medo de testar.

200w_d

Conclusão

A React Testing Library é um ótimo pacote para testar aplicações em React. Ela nos dá acesso aos matchers do jest-dom que podemos usar para testar nossos componentes de maneira mais eficiente e com boas práticas.

Espero que este artigo tenha sido útil e que ajude você a criar aplicações em React robustas no futuro.

Você pode encontrar o projeto finalizado aqui.

Obrigado pela leitura!

Leia mais artigos do autorInscreva-se na newsletter do autorSiga o autor no Twitter

Você pode ler outros artigos como este no blog do autor.

Próximos passos (documentações em inglês)

Documentação da React Testing Library

Ficha informativa da React Testing Library

Ficha informativa dos matchers do Jest DOM

Documentação do Jest