Use React Testing Library para testar a superfície do componente

Uma cartilha sobre o React Testing Library e guia para escrever testes leves para seus componentes

christoffer noring Blocked Desbloquear Seguir Seguindo 8 de janeiro

React Testing Library é uma biblioteca de testes diferente, pois testa a superfície do seu componente em vez dos internos. Você pode alterar seus componentes o quanto quiser, desde que eles renderizem os dados da mesma maneira ou o Reagir da mesma forma se você, após interações, como preencher dados ou pressionar um botão, por exemplo.

Isto é o que o autor da biblioteca Kent C. Dodds diz sobre isso:

Utilitários de teste React DOM simples e completos que incentivam boas práticas de teste

É uma solução leve para testar componentes React. Fornece funções de utilidade em cima de react-dom . Seus testes funcionam em nós DOM em oposição a instâncias do componente React.

Neste artigo, vamos cobrir o seguinte:

  • Escrevendo um teste , mostre como é simples escrever um teste, instanciar um componente e assert nele
  • Lidando com eventos , aprenderemos como podemos disparar eventos e assert sobre o componente resultante depois
  • Ações assíncronas , aprenderemos como podemos acionar e esperar que as ações assíncronas terminem
  • Gerenciar a entrada , vamos aprender como enviar pressionamentos de tecla para inserir elementos em nossos componentes e assert sobre o resultado

É fácil começar, você só precisa instalar o react-testing-library :

 fio adicionar reagir-teste-biblioteca 

Escrevendo um teste

Vamos olhar para um cenário real e ver o que queremos dizer. Nós vamos criar:

  • Todos.js é um componente que permite renderizar uma lista de Todos e selecionar um item específico do Todo.
  • Todos.test.js , nosso arquivo de teste

Nosso código de componente é assim:

 // Todos.js 
 importar Reagir de 'reagir'; 
 import './Todos.css'; 

 const Todos = ({todos, selecione, selecionado}) => ( 
 <React.Fragment> 
 {todos.map (todo => ( 
 <React.Fragment key = {todo.title}> 
 <h3 data-testid = "item" className = {selecionado && selected.title === todo.title? 'selected': ''}> {todo.title} </ h3> 
 <div> {todo.description} </ div> 
 <button onClick = {() => selecionar (todo)}> Selecionar </ button> 
 </React.Fragment> 
 ))} 
 </React.Fragment> 
 ); 
 classe TodosContainer estende React.Component { 
 state = { 
 todo: void 0, 
 } 
 select = (todo) => { 
 this.setState ({ 
 façam, 
 }) 
 } 
 render () { 
 Retorna ( 
 <Todos {… this.props} select = {this.select} selecionado = {this.state.todo} /> 
 ); 
 } 
 } 
 exportação default TodosContainer; 

Agora para o teste:

 // Todos.test.js 
 import {render, fireEvent, wait} de 'react-testing-library'; 
import 'reagir-teste-biblioteca / limpeza-depois-cada';
 importar Reagir de 'reagir'; 
 import 'jest-dom / extend-expect'; 
 import Todos a partir de '../Todos'; 

const todos = [
{
 título: 'todo1' 
 } 
 { 
 título: 'todo2' 
 }]; 

describe ('Todos', () => {
 it ('encontra o título', () => { 
 const {getByText, getByTestId, container} = renderizar (<todos todos = {todos} />); 
 }) 
 }); 

Podemos ver pelo código acima que estamos usando alguns ajudantes de react-testing-library :

  • render () , isso renderizará nosso componente
  • fireEvent , isso nos ajudará a acionar coisas como um evento de click ou alterar os dados de entrada, por exemplo
  • espere , isso nos permite esperar que um elemento apareça

Olhando para o teste em si, vemos que quando chamamos render , recuperamos um objeto e destruímos 3 valores dele:

 const {getByText, getByTestId, container} = renderizar (<todos todos = {todos} />) 

e acabamos com os seguintes ajudantes:

  • getByText , este pega um elemento pelo seu conteúdo de texto
  • getByTestId , isso pega um elemento por data-testid , então se você tem um atributo em seu elemento como então data-testid=”saved” você estaria consultando como assim getByTestId('saved')
  • recipiente , o div seu componente foi processado para

Vamos preencher esse teste:

 // Todos.test.js 
 import {render, fireEvent, wait} de 'react-testing-library'; 
 importar Reagir de 'reagir'; 
 import 'jest-dom / extend-expect'; 
import 'reagir-teste-biblioteca / limpeza-depois-cada';
 import Todos a partir de '../Todos'; 

 const todos = [ 
{
 título: 'todo1' 
 } 
 { 
 título: 'todo2' 
 }]; 

describe ('Todos', () => {
 it ('encontra o título', () => { 
 const {getByText, getByTestId, container} = renderizar (<todos todos = {todos} />); 
 const elem = getByTestId ('item'); 
 esperar (elem.innerHTML) .toBe ('todo1'); 
 }) 
 }); 

Como podemos ver acima, podemos renderizar nosso componente e consultar um elemento h3 usando o container e o querySelector . Finalmente, afirmamos o texto dentro do elemento.

Manipulando ações

Vamos dar uma olhada no nosso componente novamente. Ou melhor, vamos olhar um trecho dele:

 // trecho de Todos.js 
 const Todos = ({todos, selecione, selecionado}) => ( 
 <React.Fragment> 
 {todos.map (todo => ( 
 <React.Fragment key = {todo.title}> 
 <h3 className = {selected && selected.title === todo.title? 'selected': ''}> {todo.title} </ h3> 
 <div> {todo.description} </ div> 
 <button onClick = {() => selecionar (todo)}> Selecionar </ button> 
 </React.Fragment> 
 ))} 
 </React.Fragment> 
 ); 

Vemos acima que tentamos definir a classe CSS para selected se um todo for selecionado. A maneira de obter um todo selecionado é clicar nele, podemos ver como invocamos o método select quando clicamos no botão que é renderizado, um por item. Vamos tentar testar isso adicionando um teste:

 import {render, fireEvent, wait} de 'react-testing-library' 
 importar Reagir de 'reagir'; 
 import 'jest-dom / extend-expect' 
import 'reagir-teste-biblioteca / limpeza-depois-de cada'
 import Todos a partir de '../Todos'; 
 const todos = [ 
{
 título: 'todo1' 
 } 
 { 
 título: 'todo2' 
 } 
];

 describe ('Todos', () => { 
 it ('encontra o título', () => { 
 const {getByText, getByTestId, container} = renderizar (<todos todos = {todos} />); 
 const elem = getByTestId ('item'); 
 esperar (elem.innerHTML) .toBe ('todo1'); 
 }) 

 isso ('select todo', () => { 
 const {getByText, getByTestId, container} = renderizar (<todos todos = {todos} />); 
 fireEvent.click (getByText ('Select')); 
 const elem = getByTestId ('item'); 
 expect (elem.classList [0]). toBe ('selecionado'); 
 }) 
 }); 

Nosso último teste recém-adicionado está usando o assistente fireEvent para executar um click e podemos ver que estamos usando o auxiliar getByText para encontrar o botão. Novamente, usamos o container para localizar e declarar a classe CSS selected .

Testes assíncronos e trabalho com entrada

Até agora, mostramos a você como renderizar um componente, encontrar os elementos resultantes e afirmar sobre eles. Também mostramos como você pode realizar coisas como clicar em um botão. Nesta seção, mostraremos duas coisas:

  • Manipulando entrada
  • Lidando com ações assíncronas

Nós construiremos o seguinte:

  • Note.js , um componente que nos permite inserir dados e salvar os resultados, ele também nos permite buscar dados
  • __tests __ / Note.js , o arquivo de teste

Vamos dar uma olhada no componente:

 // Note.js 

importar Reagir de 'reagir';

nota de classe estende React.Component {
 state = { 
 conteúdo: '', 
 salvou: '', 
 }; 
 onChange = (evt) => { 
 this.setState ({ 
 conteúdo: evt.target.value, 
 }); 
 console.log ('atualizando conteúdo'); 
 } 
 save = () => { 
 this.setState ({ 
 salvo: `Salvo: $ {this.state.content}`, 
 }); 
 } 
 load = () => { 
 var me = isto; 
 setTimeout (() => { 
 me.setState ({ 
 dados: [{title: 'test'}, {title: 'test2'}] 
 }) 
 }, 3000); 
 } 
 render () { 
 Retorna ( 
 <React.Fragment> 
 <label htmlFor = "alterar"> Alterar texto </ label> 
 <input id = "change" placeholder = "alterar texto" onChange = {this.onChange} /> 
 <div data-testid = "salvo"> {this.state.saved} </ div> 
 {this.state.data && 
 <div data-testid = "data"> 
 {this.state.data.map (item => ( 
 <div className = "item"> {item.title} </ div> 
 ))} 
 </ div> 
 } 
 <div> 
 <button onClick = {this.save}> Salvar </ button> 
 <button onClick = {this.load}> Carregar </ button> 
 </ div> 
 </React.Fragment> 
 ); 
 } 
 } 
 nota padrão de exportação; 

Manipulando a entrada do usuário

Para salvar dados em nosso aplicativo de amostra, inserimos o texto em uma entrada e pressionamos o botão salvar. Vamos criar um teste para isso:

 // __tests__ /Note.js 
 import {render, fireEvent, wait} de 'react-testing-library' 
 importar Reagir de 'reagir'; 
 import 'jest-dom / extend-expect' 
import 'reagir-teste-biblioteca / limpeza-depois-de cada'
 import Selecione de '../Note'; 

 descrever ('Note', () => { 
 it ('salvar texto', async () => { 
 const {getByText, getByTestId, getByPlaceholderText, container, getByLabelText} = render (<Selecionar />); 
 const input = getByLabelText ('Alterar texto'); 
 input.value = 'texto de entrada'; 
 fireEvent.change (entrada); 
 fireEvent.click (getByText ('Salvar')); 
 console.log ('salvo', getByTestId ('salvo'). innerHTML); 
 expect (getByTestId ('saved')). toHaveTextContent ('texto de entrada') 
 }) 
 }); 

Podemos ver acima que usamos o auxiliar getByLabelText para obter uma referência à nossa entrada e simplesmente fazemos input.value = 'input text' nesse ponto. Então precisamos invocar fireEvent.change(input) para que a mudança aconteça. Depois disso, podemos afirmar os resultados digitando expect(getByTestId('saved')).toHaveTextContent('input text')

Lidando com código assíncrono

Temos outra funcionalidade em nosso componente, que é pressionar um botão Load que chama um método load() , da seguinte forma:

 load = () => { 
 var me = isto; 
 setTimeout (() => { 
 me.setState ({ 
 dados: [{title: 'test'}, {title: 'test2'}] 
 }) 
 }, 3000); 
 } 

Podemos ver acima que a mudança não acontece imediatamente, isso devido a nós usando um setTimeout() . Observando nosso componente, podemos ver que não renderizamos a propriedade de data , a menos que ela esteja configurada para um valor:

 {this.state.data && 
 <div data-testid = "data"> 
 {this.state.data.map (item => ( 
 <div className = "item"> {item.title} </ div> 
 ))} 
 </ div> 
 } 

Nosso teste precisa atender a isso e esperar que o div com atributo data-testid="data” esteja presente antes que ele possa afirmar sobre ele. Isso pode ser tratado através de async/await waitForElement . Nós importamos waitForElement de waitForElement react-testing-library que nos permite interromper a execução enquanto aguardamos a exibição do elemento. Vamos ver como é isso, adicionando um teste ao nosso arquivo de teste:

 importar { 
render,
fireEvent,
esperar,
waitForElement,
} de 'react-testing-library'
import 'reagir-teste-biblioteca / limpeza-depois-cada';
 importar Reagir de 'reagir'; 
 import 'jest-dom / extend-expect' 
 import Selecione de '../Note'; 

 descrever ('Note', () => { 
 it ('salvar texto', async () => { 
 const {getByText, getByTestId, getByPlaceholderText, container, getByLabelText} = render (<Selecionar />); 
 const input = getByLabelText ('Alterar texto'); 
 input.value = 'texto de entrada'; 
 fireEvent.change (entrada); 
 fireEvent.click (getByText ('Salvar')); 
 console.log ('salvo', getByTestId ('salvo'). innerHTML); 
 expect (getByTestId ('saved')). toHaveTextContent ('texto de entrada') 
 }) 

 ele ('load data', async () => { 
 const {getByText, getByTestId, getByPlaceholderText, container} = render (<Selecionar />); 
 fireEvent.click (getByText ('Load')); 
 const elem = await waitForElement (() => getByTestId ('dados')) 
 const elem = getByTestId ('item'); 
 esperar (elem) .toHaveTextContent ('test'); 
 }) 
 }); 

Acima, vemos a construção await waitForElement(() => getByTestId('data')) que impede que o teste continue até que o elemento esteja presente. O waitForElement retorna uma promessa que não é resolvida até que o elemento exista no DOM. Depois disso, afirmamos o resultado.

Resumo

Analisamos a react-testing-library e escrevemos testes cobrindo os principais casos de uso. Aprendemos a lidar com eventos, ações assíncronas, como gerenciar a entrada do usuário. Nós cobrimos a maioria das coisas que esta biblioteca tem a oferecer, mas mais importante, nós aprendemos a pensar sobre os testes de uma maneira diferente.

Talvez não tenhamos que testar os internos, mas sim a superfície de nossos componentes?

Leitura adicional

Há muito mais para esta biblioteca e você é encorajado a olhar para o