Pular para o conteúdo

c

Testando aplicações React

Existem algumas formas de testar aplicações React. Vamos dar uma olhada em algumas delas a frente.

Testes serão implementados com a mesma Jest biblioteca de teste desenvolvida pelo Facebook que foi usada na parte anterior. Jest é configurado por padrão para aplicações criadas com o create-react-app.

Além do JEST, também precisamos de outra biblioteca de testes que nos ajude a renderizar componentes para fins de teste. A melhor opção atual para isso é react-test-library que sofreu um rápido crescimento de popularidade nos últimos tempos.

Vamos instalar a biblioteca com o comando:

npm install --save-dev @testing-library/react @testing-library/jest-dom

Também instalamos jest-dom, que fornece alguns métodos auxiliares relacionados a Jest.

Vamos primeiro escrever testes para o componente responsável por renderizar uma nota:

const Note = ({ note, toggleImportance }) => {
  const label = note.important
    ? 'make not important'
    : 'make important'

  return (
    <li className='note'>      {note.content}
      <button onClick={toggleImportance}>{label}</button>
    </li>
  )
}

Observe que o elemento li possui o CSS className note , que poderia ser usado para acessar o componente em nossos testes.

Renderizando o componente para testes

Escreveremos nosso teste no arquivo src/components/note.test.js , que está no mesmo diretório que o próprio componente.

O primeiro teste verifica que o componente renderiza o conteúdo da nota:

import React from 'react'
import '@testing-library/jest-dom/extend-expect'
import { render, screen } from '@testing-library/react'
import Note from './Note'

test('renders content', () => {
  const note = {
    content: 'Component testing is done with react-testing-library',
    important: true
  }

  render(<Note note={note} />)

  const element = screen.getByText('Component testing is done with react-testing-library')
  expect(element).toBeDefined()
})

Após a configuração inicial, o teste renderiza o componente com o render que é uma função fornecida pela react-testing-library:

render(<Note note={note} />)

Normalmente, os componentes React são renderizados no DOM . O método de renderização que usamos renderiza os componentes em um formato adequado para testes sem renderizá-los ao DOM.

Podemos usar o objeto screen para acessar o componente renderizado. Usamos o método da Screen getByText para procurar um elemento que tenha o conteúdo da nota e garantir que ele exista:

  const element = screen.getByText('Component testing is done with react-testing-library')
  expect(element).toBeDefined()

Testes de execução

Create-react-app configura os testes a serem executados no modo Watch por padrão, o que significa que o comando npm test não terminará assim que os testes terminarem e, em vez disso, aguardará as alterações a serem feitas no código. Depois que novas alterações no código são salvas, os testes são executados automaticamente, depois disso Jest volta a aguardar que novas alterações sejam feitas.

Se você deseja executar testes "normalmente", pode fazê-lo com o comando:

CI=true npm test

Para usuários do Windows (PowerShell)

$env:CI=$true; npm test

Obs: O console pode emitir um aviso se você não tiver instalado o Watchman. O Watchman é uma aplicação desenvolvida pelo Facebook que observa as alterações feitas nos arquivos. O programa acelera a execução dos testes e pelo menos a partir do MacOS Sierra, executando testes no modo watch emite alguns avisos no console, que podem ser removidos instalando o Watchman.

As instruções para instalar o Watchman em diferentes sistemas operacionais podem ser encontradas no site oficial do Watchman: https://facebook.github.io/watchman/

Localização do arquivo de teste

No React, existem (pelo menos) duas convenções diferentes para a localização do arquivo de teste. Criamos nossos arquivos de teste de acordo com o padrão atual, colocando -os no mesmo diretório que o componente que está sendo testado.

A outra convenção é armazenar os arquivos de teste "normalmente" em um diretório test separado. Qualquer que seja a convenção que escolhemos, é quase garantido que esteja errado de acordo com a opinião de alguém.

Não gosto dessa maneira de armazenar testes e código de aplicações no mesmo diretório. O motivo pelo qual escolhemos seguir esta convenção é que ela é configurada por padrão em aplicações criados pelo Create-React-App.

Procurando conteúdo em um componente

O pacote react-testing-library oferece muitas maneiras diferentes de investigar o conteúdo do componente que está sendo testado. Na realidade, o expect em nosso teste não é necessário.

import React from 'react'
import '@testing-library/jest-dom/extend-expect'
import { render, screen } from '@testing-library/react'
import Note from './Note'

test('renders content', () => {
  const note = {
    content: 'Component testing is done with react-testing-library',
    important: true
  }

  render(<Note note={note} />)

  const element = screen.getByText('Component testing is done with react-testing-library')

  expect(element).toBeDefined()})

O teste falha se getByText não encontrar o elemento que está procurando.

Também poderíamos usar CSS-selectors para encontrar elementos renderizados usando o método queryselector do objeto container que é um dos campos retornados pela renderização:

import React from 'react'
import '@testing-library/jest-dom/extend-expect'
import { render, screen } from '@testing-library/react'
import Note from './Note'

test('renders content', () => {
  const note = {
    content: 'Component testing is done with react-testing-library',
    important: true
  }

  const { container } = render(<Note note={note} />)
  const div = container.querySelector('.note')  expect(div).toHaveTextContent(    'Component testing is done with react-testing-library'  )})

Existem também outros métodos, por exemplo, getByTestId, que procuram elementos com base em id-attributes que são inseridos no código especificamente para fins de teste.

Testes de depuração

Normalmente, encontramos muitos tipos diferentes de problemas ao escrever nossos testes.

Objeto screen possui método debug que pode ser usado para imprimir o HTML de um componente para o terminal. Se alterarmos o teste da seguinte forma:

import React from 'react'
import '@testing-library/jest-dom/extend-expect'
import { render, screen } from '@testing-library/react'
import Note from './Note'

test('renders content', () => {
  const note = {
    content: 'Component testing is done with react-testing-library',
    important: true
  }

  render(<Note note={note} />)

  screen.debug()
  // ...

})

O HTML é impresso no console:

console.log
  <body>
    <div>
      <li
        class="note"
      >
        Component testing is done with react-testing-library
        <button>
          make not important
        </button>
      </li>
    </div>
  </body>

Também é possível usar o mesmo método para imprimir um elemento procurado para consolar:

import React from 'react'
import '@testing-library/jest-dom/extend-expect'
import { render, screen } from '@testing-library/react'
import Note from './Note'

test('renders content', () => {
  const note = {
    content: 'Component testing is done with react-testing-library',
    important: true
  }

  render(<Note note={note} />)

  const element = screen.getByText('Component testing is done with react-testing-library')

  screen.debug(element)
  expect(element).toBeDefined()
})

Agora o HTML do elemento procurado é impresso:

  <li
    class="note"
  >
    Component testing is done with react-testing-library
    <button>
      make not important
    </button>
  </li>

Botões de clique em testes

Além de exibir conteúdo, o componente Nota também garante que, quando o botão associado à nota é pressionado, a função que manipula eventos (event handler) toggleImportance é chamada.

Vamos instalar uma biblioteca user-event que facilita a simulação de entrada do usuário:

npm install --save-dev @testing-library/user-event

Testando essa funcionalidade pode ser realizada assim:

import React from 'react'
import '@testing-library/jest-dom/extend-expect'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'import Note from './Note'

// ...

test('clicking the button calls event handler once', async () => {
  const note = {
    content: 'Component testing is done with react-testing-library',
    important: true
  }

  const mockHandler = jest.fn()

  render(
    <Note note={note} toggleImportance={mockHandler} />
  )

  const user = userEvent.setup()
  const button = screen.getByText('make not important')
  await user.click(button)

  expect(mockHandler.mock.calls).toHaveLength(1)
})

Existem algumas coisas interessantes relacionadas a este teste. O event handler é uma função mock definida com jest:

const mockHandler = jest.fn()

Uma sessão é iniciada para interagir com o componente renderizado:

const user = userEvent.setup()

O teste encontra o botão com base no texto do componente renderizado e clica no elemento:

const button = screen.getByText('make not important')
await user.click(button)

Clicar acontece com o método click da biblioteca userevent-library.

A expectativa do teste verifica que a função mock foi chamada exatamente uma vez.

expect(mockHandler.mock.calls).toHaveLength(1)

Objetos e funções mock são componentes omumente usados nos testes para substituir as dependências dos componentes que estão sendo testados. Mocks possibilitam retornar respostas codificadas e verificar o número de vezes que as funções mocks são chamadas e com quais parâmetros.

Em nosso exemplo, a função mock é uma escolha perfeita, desde que ela possa ser facilmente usada para verificar se o método é chamado exatamente uma vez.

Testes para o componente Togglable

Vamos escrever alguns testes para o componente Togglable. Vamos adicionar o o nome de classe de css togglableContent ao DIV que retorna os componentes filhos.

const Togglable = forwardRef((props, ref) => {
  // ...

  return (
    <div>
      <div style={hideWhenVisible}>
        <button onClick={toggleVisibility}>
          {props.buttonLabel}
        </button>
      </div>
      <div style={showWhenVisible} className="togglableContent">        {props.children}
        <button onClick={toggleVisibility}>cancel</button>
      </div>
    </div>
  )
})

Os testes são mostrados abaixo:

import React from 'react'
import '@testing-library/jest-dom/extend-expect'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Togglable from './Togglable'

describe('<Togglable />', () => {
  let container

  beforeEach(() => {
    container = render(
      <Togglable buttonLabel="show...">
        <div className="testDiv" >
          togglable content
        </div>
      </Togglable>
    ).container
  })

  test('renders its children', async () => {
    await screen.findAllByText('togglable content')
  })

  test('at start the children are not displayed', () => {
    const div = container.querySelector('.togglableContent')
    expect(div).toHaveStyle('display: none')
  })

  test('after clicking the button, children are displayed', async () => {
    const user = userEvent.setup()
    const button = screen.getByText('show...')
    await user.click(button)

    const div = container.querySelector('.togglableContent')
    expect(div).not.toHaveStyle('display: none')
  })
})

A função beforeEach é chamada antes de cada teste, o que renderiza o componente Togglable e salva o campo container do valor de retorno.

O primeiro teste verifica que o componente Togglable renderiza seu componente filho

<div className="testDiv">
  conteúdo alternável
</div>

Os testes restantes usam o método toHaveStyle para verificar se o componente filho do componente Togglable não é visível inicialmente, verificando se o estilo do elemento div contém { display: 'none' }. Outro teste verifica que, quando o botão é pressionado, o componente é visível, o que significa que o estilo para ocultar o componente não é mais atribuído ao componente.

Vamos também adicionar um teste que pode ser usado para verificar se o conteúdo visível pode ser oculto clicando no segundo botão do componente:

describe('<Togglable />', () => {

  // ...

  test('toggled content can be closed', async () => {
    const user = userEvent.setup()
    const button = screen.getByText('show...')
    await user.click(button)

    const closeButton = screen.getByText('cancel')
    await user.click(closeButton)

    const div = container.querySelector('.togglableContent')
    expect(div).toHaveStyle('display: none')
  })
})

Testando os formulários

Já usamos a função Click do user-event em nossos testes anteriores para clicar em botões.

const user = userEvent.setup()
const button = screen.getByText('show...')
await user.click(button)

Também podemos simular a entrada de texto com userEvent.

Vamos fazer um teste para o componente NoteForm. O código do componente é o seguinte.

import { useState } from 'react'

const NoteForm = ({ createNote }) => {
  const [newNote, setNewNote] = useState('')

  const handleChange = (event) => {
    setNewNote(event.target.value)
  }

  const addNote = (event) => {
    event.preventDefault()
    createNote({
      content: newNote,
      important: Math.random() > 0.5,
    })

    setNewNote('')
  }

  return (
    <div className="formDiv">
      <h2>Create a new note</h2>

      <form onSubmit={addNote}>
        <input
          value={newNote}
          onChange={handleChange}
        />
        <button type="submit">save</button>
      </form>
    </div>
  )
}

export default NoteForm

O formulário funciona chamando a função createNote que ele recebeu como adereços com os detalhes da nova nota.

O teste é o seguinte:

import React from 'react'
import { render, screen } from '@testing-library/react'
import '@testing-library/jest-dom/extend-expect'
import NoteForm from './NoteForm'
import userEvent from '@testing-library/user-event'

test('<NoteForm /> updates parent state and calls onSubmit', async () => {
  const createNote = jest.fn()
  const user = userEvent.setup()

  render(<NoteForm createNote={createNote} />)

  const input = screen.getByRole('textbox')
  const sendButton = screen.getByText('save')

  await user.type(input, 'testing a form...')
  await user.click(sendButton)

  expect(createNote.mock.calls).toHaveLength(1)
  expect(createNote.mock.calls[0][0].content).toBe('testing a form...')
})

Os testes têm acesso ao campo de entrada usando a função getByRole.

O método type do userEvent é usado para escrever texto no campo de entrada.

A primeira expectativa de teste garante que o envio do formulário chama o método createNote. A segunda expectativa verifica, que o event handler é chamado com os parâmetros corretos - que uma nota com o conteúdo correta é criada quando o formulário é preenchido.

Sobre encontrar os elementos

Vamos supor que o formulário tenha dois campos de entrada

const NoteForm = ({ createNote }) => {
  // ...

  return (
    <div>
      <h2>Create a new note</h2>

      <form onSubmit={addNote}>
        <input
          value={newNote}
          onChange={handleChange}
        />
        <input          value={...}          onChange={...}        />        <button type="submit">save</button>
      </form>
    </div>
  )
}

Agora a abordagem que nosso teste usa para encontrar o campo de entrada

const input = screen.getByRole('textbox')

causaria um erro:

Erro do nó que mostra dois elementos com caixa de texto, já que usamos getByRole

A mensagem de erro sugere usar getAllByRole. O teste pode ser corrigido da seguinte maneira:

const inputs = screen.getAllByRole('textbox')

await user.type(inputs[0], 'testing a form...')

Método getAllByRole agora retorna uma matriz e o campo de entrada correto é o primeiro elemento da matriz. No entanto, essa abordagem é um pouco suspeita, pois se baseia na ordem dos campos de entrada.

Muitas vezes, os campos de entrada têm um placeholder que sugere o usuário que tipo de entrada é esperada. Vamos adicionar um espaço reservado ao nosso formulário:

const NoteForm = ({ createNote }) => {
  // ...

  return (
    <div>
      <h2>Create a new note</h2>

      <form onSubmit={addNote}>
        <input
          value={newNote}
          onChange={handleChange}
          placeholder='write note content here'        />
        <input
          value={...}
          onChange={...}
        />    
        <button type="submit">save</button>
      </form>
    </div>
  )
}

Agora, encontrar o campo de entrada certo é fácil com o método getByPlaceholderText:

test('<NoteForm /> updates parent state and calls onSubmit', () => {
  const createNote = jest.fn()

  render(<NoteForm createNote={createNote} />) 

  const input = screen.getByPlaceholderText('write here note content')  const sendButton = screen.getByText('save')

  userEvent.type(input, 'testing a form...')
  userEvent.click(sendButton)

  expect(createNote.mock.calls).toHaveLength(1)
  expect(createNote.mock.calls[0][0].content).toBe('testing a form...')
})

A maneira mais flexível de encontrar elementos nos testes é o método querySelector do objeto container, que é retornado por render, como foi mencionado anteriormente nesta parte. Qualquer seletor de CSS pode ser usado com esse método para pesquisar elementos nos testes.

Considere por exemplo. que definiríamos um _id _id único para o campo de entrada:

const NoteForm = ({ createNote }) => {
  // ...

  return (
    <div>
      <h2>Create a new note</h2>

      <form onSubmit={addNote}>
        <input
          value={newNote}
          onChange={handleChange}
          id='note-input'        />
        <input
          value={...}
          onChange={...}
        />    
        <button type="submit">save</button>
      </form>
    </div>
  )
}

O elemento input agora pode ser encontrado no teste da seguinte forma:

const { container } = render(<NoteForm createNote={createNote} />)

const input = container.querySelector('#note-input')

No entanto, seguiremos a abordagem de usar getByPlaceholderText no teste.

Vejamos alguns detalhes antes de seguir em frente. Vamos supor que um componente renderia o texto para um elemento HTML da seguinte maneira:

const Note = ({ note, toggleImportance }) => {
  const label = note.important
    ? 'make not important' : 'make important'

  return (
    <li className='note'>
      Your awesome note: {note.content}      <button onClick={toggleImportance}>{label}</button>
    </li>
  )
}

export default Note

O comando getByText que o teste usa faz não Encontre o elemento

test('renders content', () => {
  const note = {
    content: 'Does not work anymore :(',
    important: true
  }

  render(<Note note={note} />)

  const element = screen.getByText('Does not work anymore :(')

  expect(element).toBeDefined()
})

Command getByText procura um elemento que tenha o mesmo texto que possui como parâmetro e nada mais. Se quisermos procurar um elemento que contém o texto, poderíamos usar uma opção extra:

const element = screen.getByText(
  'Does not work anymore :(', { exact: false }
)

ou poderíamos usar o comando findByText:

const element = await screen.findByText('Does not work anymore :(')

É importante notar que, diferentemente dos outros comandos ByText, findByText retorna uma promessa!

Existem situações em que mais uma forma do comando queryByText é útil. O comando retorna o elemento, mas não causa uma exceção se o elemento não for encontrado.

Nós poderíamos por exemplo. Use o comando para garantir que algo não seja renderizado ao componente:

test('does not render this', () => {
  const note = {
    content: 'This is a reminder',
    important: true
  }

  render(<Note note={note} />)

  const element = screen.queryByText('do not want this thing to be rendered')
  expect(element).toBeNull()
})

Cobertura de teste

Podemos descobrir facilmente a cobertura de nossos testes executando-os com o comando.

CI=true npm test -- --coverage
saída do terminal da cobertura de teste

Um relatório HTML bastante primitivo será gerado para o diretório coverage/lcov-report. O relatório nos dirá as linhas de código não testado em cada componente:

Relatório HTML da cobertura do teste

Você pode encontrar o código para nossa aplicação atual na íntegra em part5-8 deste repositório do github.

Testes de integração de front -end

Na parte anterior do material do curso, escrevemos testes de integração para o back-end que testou sua lógica e conectou o banco de dados através da API fornecida pelo back-end. Ao escrever esses testes, tomamos a decisão consciente de não escrever testes de unidade, pois o código para esse back-end é bastante simples, e é provável que os bugs em nossa aplicação ocorram em cenários mais complicados do que os testes de unidade adequados.

Até agora, todos os nossos testes para o frontend foram testes de unidade que validaram o funcionamento correto de componentes individuais. Às vezes, o teste de unidade é útil, mas mesmo um conjunto abrangente de testes de unidade não é suficiente para validar que o aplicação funciona como um todo.

Também poderíamos fazer testes de integração para o front-end. Testes de integração testa a colaboração de vários componentes. É consideravelmente mais difícil do que os testes de unidade, pois teríamos que, por exemplo, por exemplo mockar dados do servidor. Optamos por nos concentrar em fazer testes de ponta a ponta para testar todo a aplicação. Trabalharemos nos testes de ponta a ponta no último capítulo desta parte.

Teste de Snapshot

O JEST oferece uma alternativa completamente diferente aos chamados testes "tradicionais" snapshot. A característica interessante dos snapshots é que os desenvolvedores não precisam definir nenhum teste, é simples o suficiente para adotar testes snapshot.

O princípio fundamental é comparar o código HTML definido pelo componente depois de alterar para o código HTML que existia antes de ser alterado.

Se o snapshot perceber alguma alteração no HTML definido pelo componente, será uma nova funcionalidade ou um "bug" causado por acidente. Os testes de snapshot notificam o desenvolvedor se o código HTML do componente mudar. O desenvolvedor deve dizer a JEST se a alteração foi desejada ou indesejada. Se a alteração no código HTML for inesperada, ela implicará fortemente um bug, e o desenvolvedor poderá tomar conhecimento desses problemas em potencial, graças facilmente aos testes de snapshot.