Artigo original: How to Test React Components: the Complete Guide

Quando eu comecei aprender a testar minhas aplicações antigamente, fiquei muito frustrado com os diferentes tipos, estilos e tecnologias usadas para fazer testes, junto com os inúmeros posts espalhados pelos blogs, tutoriais e artigos. Eu penso que isso é verdade, também, para testagem em React.

Então, eu decidi escrever um guia completo de testagem em React em um só artigo.

Você poderia me perguntar: "Guia completo? Então, você vai cobrir todas as possibilidades e cenários de teste?" A resposta é: claro, que não! Porém, será um guia completo de fundamentos para testar e será o suficiente para construir a maioria para os outros casos extremos.

Também selecionei uma grande coleção de publicações em blogs, artigos e tutoriais para uma seção de leitura complementar ao final que deve dar a você conhecimento suficiente para entrar no top 10% de desenvolvedores em termos de testagem.

Você pode encontrar o projeto completo aqui:

https://github.com/iqbal125/react-hooks-testing-complete

Tabela de conteúdos

Teoria

  • O que é a testagem?
  • Por que testar?
  • O que testar?
  • O que não testar?
  • Como testar
  • Testes com shallow x testes com mount
  • Testes unitários x testes de integração x testes de ponta a ponta

Informações preliminares

  • Algumas informações extras

Enzyme

  • Configuração do Enzyme
  • react-test-renderer
  • Testes de snapshot
  • Implementação dos detalhes da testagem

Biblioteca de testagem no React

  • useState e props
  • useReducer()
  • useContext()
  • Componentes de formulário controlados
  • useEffect() e requisições para a API do Axios

Cypress

  • Uma testagem completa de ponta a ponta

Integração contínua

  • Travis.yml
  • Cobertura de código com coveralls

Teoria

O que é testagem?

Vamos começar pelo início e discutir o que é testagem. Testagem é um processo de três passos que se parece assim:

image-12
Em português: organização -> ação -> declaração

Organização é o estado original em que se encontra sua aplicação. Ação é alguma coisa acontece (como um evento de clique, entrada de usuário e assim por diante). Então, ocorre a declaração, ou é criada uma hipótese, sobre o novo estado da aplicação. Os testes passarão se a hipótese estiver correta e falharão se estiver errada.

Ao contrário dos componentes do React, os testes não são executados no navegador. Jest é o executor de testes e, também, o framework utilizado pelo React. O Jest é o ambiente onde todos os seus testes serão executados. É por isso que você não precisa importar expect e describe dentro desse arquivo. Essas funções já estão disponíveis globalmente no ambiente do Jest.

A sintaxe dos testes terá a seguinte aparência:

describe('Teste de soma', () => {
    function sum(a, b) {
       return a + b;
    }

    it('deve ser igual a 4',()=>{
       expect(sum(2,2)).toBe(4);
      })

    test('também deve ser igual a 4', () => {
        expect(sum(2,2)).toBe(4);
      }) 
});

describe envolve nossos blocos it ou test. É uma maneira de agrupá-los. Tanto it quanto test são palavras-chaves que podem ser usadas intercambiadas. O texto entre aspas descreverá algo que deveria acontecer com os testes e que será impressa no console. toBe() é um combinador que funciona com expectativas que permitem fazer declarações. Existem mais combinadores e variáveis globais oferecidas pelo Jest. Clique nos links abaixo para ver uma lista completa.

https://jestjs.io/pt-BR/docs/using-matchers

https://jestjs.io/pt-BR/docs/api

Por que testar?

A testagem é feita para garantir que sua aplicação está funcionando como foi planejada para seus usuários finais. Ter uma testagem fará com que sua aplicação fique mais robusta e menos propensa a erros. É uma maneira de verificar que seu código está fazendo o que os desenvolvedores projetaram.

Potenciais desvantagens:

  • Escrever testes consome muito tempo e é difícil
  • Em certos cenários, executar testes em Integração Contínua (em inglês, Continuous Integration, ou CI) pode ter um custo em dinheiro.
  • Se feito incorretamente, pode gerar falsos positivos. Os testes passarão, mas a aplicação não funcionará como se espera.
  • Pode gerar falsos negativos. Os testes falharão, mas a aplicação funcionará como pretendida.

O que testar?

Ao construí-los cobrindo o último estado da aplicação, os testes devem testar a funcionalidade da aplicação, simulando como ele será utilizado pelos usuários finais. Isso dará a você a confiança de que a aplicação funcionará como projetado em ambiente de produção. Naturalmente, entraremos em muito mais detalhes durante este artigo, mas essa é a essência básica.

O que não testar?

Eu gosto da filosofia de Kent C. Dodds, que diz que você não deve testar detalhes da implementação.

Detalhes da implementação representam testar coisas que não são funcionalidades do usuário final. Veremos um exemplo disso na seção do Enzyme mais adiante.

Parece que você está testando funcionalidades, mas, na verdade, não está. Você está testando o nome da função. Você pode alterar o nome da função e seus testes falharão, mas sua aplicação continuará funcionando, dando a você um falso negativo.

Preocupar-se constantemente com nomes de funções e variáveis é uma dor de cabeça. Ter que reescrever testes toda vez que você muda os nomes é tedioso. Então, mostrarei aqui uma abordagem melhor.

Variáveis const: essas variáveis são imutáveis. Você não tem a necessidade de testá-las.

Bibliotecas externas: não é seu trabalho testá-las. Quem precisa fazer isso são os criadores delas. Se você não tem certeza se a biblioteca foi testada, você não deve usá-la ou deve ler o código-fonte para ver se o autor incluiu testes. Você também pode fazer o download do código-fonte e rodar os testes você mesmo. Uma alternativa é perguntar ao autor se a biblioteca está pronta para produção ou não.

Minha abordagem pessoal para a testagem

Muito de minha abordagem pessoal de testagem é baseada nos ensinamentos de Kent C. Dodds. Então, você perceberá um pouco das falas de Kent ecoadas aqui, juntamente com algumas ideias minhas.

Muitos testes de integração, nenhum teste de snapshot, poucos testes unitários e poucos testes de ponta a ponta.

Testes unitários são um passo acima do teste de snapshot, mas não são ideais. Eles são, no entanto, muito mais fáceis de compreender e de se fazer sua manutenção do que os testes de snapshot.

Escreva, em sua maioria, testes de integração. Testes unitários são bons, mas eles não necessariamente se assemelham à maneira como o seu usuário final interage com a aplicação. É muito fácil testar detalhes de implementação com testes unitários, especialmente com a renderização superficial (em inglês, shallow render).

Testes de integração devem usar o mínimo de mocks (uma espécie de "dublê de teste"), se possível.

Não teste detalhes de implementação, como nomes de variáveis e funções.

Por exemplo, se você estiver testando um botão e muda o nome da função no método onClick de increment() para handleClick(), os testes quebrarão, mas o componente permanecerá funcionando. Essa não é uma boa prática, pois estamos basicamente testando o nome da função, que é um detalhe da implementação com o qual nosso usuário final não se importa.

Testes com shallow x testes com mount

Testes com mount(), de fato, executam o código html, css e js como um navegador faria, mas de maneira simulada. Eles não renderizam ou imprimem nada na interface, mas agem como um navegador da web simulado e executam o código em segundo plano.

Não perder tempo imprimindo nada na interface torna os testes muito mais rápidos. No entanto, testes com mount() ainda são bem mais lentos que testes com shallow().

É por isso que você desmonta ou faz a limpeza do componente depois de cada teste. É quase como uma aplicação em funcionamento. Um teste acabará afetando o outro.

Testes com mount()/render() são tipicamente usados para testes de integração, enquanto testes com shallow() são usados para testes unitários.

Os testes com shallow() apenas imprimem um único componente, aquele que estamos testando. Por não renderizar componentes filhos, conseguimos testar nossos componentes em isolamento.

Por exemplo, considere esse componente filho e o componente pai.

import React from 'react';

const App = () => {
  return (
    <div> 
      <ComponenteFilho /> 
    </div> 
  )
}

const ComponenteFilho = () => {
  return (
    <div>
     <p> Componentes filhos </p>
    </div>
  )
}

Se usarmos o teste com shallow() de App.js, teremos algo como o que vemos abaixo. Observe que nenhum dos nós do DOM para o componente filho está presente. Por isso, usamos o termo shallow render (em português, renderização superficial).

<App>
  <div> 
    <ComponenteFilho /> 
  </div>
</App> 

Agora, podemos comparar isso com a montagem do componente:

<App>
  <div> 
    <ComponenteFilho> 
      <div>
       <p> Componentes filhos </p>
      </div>
    </ComponenteFilho>
   </div>
</App> 

O que temos acima é muito mais próximo do que veremos na aplicação no navegador. Por isso, dizemos que os testes com mount()/render() são superiores.

Testes unitários x testes de integração x testes de ponta a ponta

Teste unitário: teste de uma parte isolada da aplicação, normalmente feita em combinação com o teste com shallow(). Exemplo: um componente renderizado com props (propriedades) padrão.

Teste de integração: testa se as partes distintas funcionam ou se integram corretamente entre si. Normalmente, é feito na criação ou na renderização do componente. Exemplo: teste se um componente filho pode atualizar o state contextual no componente pai.

Teste de ponta a ponta: normalmente, é uma combinação de várias etapas, combinando testes unitários e testes de integração dentro de uma etapa maior, geralmente com pouco mock (simulação). Os testes são feitos em um navegador simulado e podem ocorrer ou não enquanto o teste de interface está rodando. Exemplo: testagem de um fluxo inteiro de autenticação.

Informações preliminares

react-testing-library: eu, pessoalmente, gosto de usar a biblioteca react-testing-library, mas, normalmente, usa-se a Enzyme. Mostrarei a você um exemplo com a Enzyme, pois é importante estar ciente do básico de Enzyme. Os outros exemplos serão com react-testing-library.

Perfil dos exemplos: nossos exemplos seguem um padrão. Eu, primeiramente, mostrarei o componente do React e, então, os testes para ele, com explicações detalhadas de cada um. Você também pode acompanhar através do repositório cujo link eu deixei no início deste artigo.

Configuração: eu também assumirei que você está usando o create-react-app com a configuração de teste padrão, com jest. Assim, eu vou pular as configurações manuais.

Sinon, mocha, chai: Muitas das configurações oferecidas pelo Sinon estão disponíveis por padrão com o Jest. Assim, você não precisa do Sinon. Mocha e Chai são uma substituição para o Jest. O Jest vem pré-configurado de início para trabalhar com sua aplicação. Desse modo, não faz sentido usar o Mocha ou o Chai.

Esquema de nomeação de componentes: meu esquema de nomeação para o componente é <TesteAlgo />, mas não significa que eles sejam componentes falsos. Eles são componentes do React normais. Esse é apenas o esquema de nomeação.

npm test e o modo jest watch: yarn test funcionou para mim. npm test não funcionou corretamente com o modo jest watch.

Teste com um único arquivo: yarn test nome_do_arquivo

Hooks x classes no React: eu uso componentes com hooks do React para a maioria dos exemplos, mas, devido ao poder da react-testing-library, todos esses testes funcionarão com componentes de classes também.

Com essas informações preliminares, podemos partir para a escrita do código.

Enzyme

Configurando o Enzyme

Nossas bibliotecas externas

npm install enzyme enzyme-to-json  enzyme-adapter-react-16

Primeiro, vamos começar fazendo as importações

import React from 'react';
import ReactDOM from 'react-dom';
import Basic from '../basic_test';

import Enzyme, { shallow, render, mount } from 'enzyme';
import toJson from 'enzyme-to-json';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() })

Vamos começar nossas importações básicas. As três primeiras são para o React e para o nosso componente.

Depois, importamos o Enzyme. Em seguida, importamos a função toJson da biblioteca 'enzyme-to-json'. Vamos precisar converter nosso componente para teste com shallow() em JSON, que poderá ser salvo no arquivo do snapshot.

Finalmente, importamos nosso Adapter para fazer o Enzyme funcionar com o React 16 e o inicializamos como é mostrado abaixo.

react-test-renderer

O React, de fato, vem com seu próprio renderizador de testes. Você pode usá-lo ao invés do Enzyme se quiser. Sua sintaxe se parece com a do exemplo abaixo:

// import TestRenderer from 'react-test-renderer';
// import ShallowRenderer from 'react-test-renderer/shallow';


// Teste básico com react-test-renderer
// it('renderiza corretamente o react-test-renderer', () => {
//   const renderer = new ShallowRenderer();
//   renderer.render(<Basic />);
//   const result = renderer.getRenderOutput();
//
//   expect(result).toMatchSnapshot();
// });

Porém, até mesmo a documentação do próprio react-test-render sugere usar o Enzyme devido à sua sintaxe um pouco melhor e por ele fazer a mesma coisa. É bom ter isso em mente.

Teste de snapshot

Agora o nosso primeiro teste, um teste de snapshot.

it('renderiza corretamente o Enzyme', () => {
  const wrapper = shallow(<Basic />)

  expect(toJson(wrapper)).toMatchSnapshot();
});

Se você nunca rodou esse comando antes, um diretório __snapshots__  e um arquivo test.js.snap serão criados para você automaticamente. Em cada teste subsequente, o novo arquivo de snapshot será comparado com o arquivo de snapshot já existente. O teste passará se o snapshot não tiver alterações e falhará se elas existirem.

Então, essencialmente, o teste de snapshot permite você ver como seu componente se alterou desde o último teste, linha por linha. As linhas de código que sofreram mudanças são conhecidas como diff, abreviação em inglês para diferenças.

Aqui está o componente básico em que realizamos o teste:

import React from 'react';


const Basic = () => {
  return (
    <div >
      <h1> Basic Test </h1>
         <p> This is a basic Test Component </p>
    </div>
  );
}

export default Basic;


Rodar o teste acima gerará um arquivo que contém um código parecido com o que é mostrado abaixo. Essa é, essencialmente, nossa árvore de nós do React DOM.

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renderiza corretamente o enzyme 1`] = `
<div>
  <h1>
     Basic Test
  </h1>
  <p>
     This is a basic Test Component
  </p>
</div>
`;

Isso produzirá uma estrutura de diretórios como a mostrada abaixo:

image-6

Seu terminal gerará um resultado parecido com este:

image-7

Entretanto, o que acontece se mudarmos nosso componente básico para isso:

import React from 'react';


const Basic = () => {
  return (
    <div >
      <h1> Basic Test </h1>

    </div>
  );
}

export default Basic;

Nosso snapshot falhará.

image-8

Ele também nos mostrará a diferença.

image-9

Assim como no git, o sinal " - " antes de cada linha significa que ela foi removida.

Nós precisamos apenas apertar "w" para ativar o modo de observação (do inglês, watch) e, então, pressionar a tecla "u" (do inglês, update) para atualizar o snapshot.

Nosso arquivo de snapshot será atualizado automaticamente com o novo snapshot e passará nos testes.

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renderiza corretamente o Enzyme 1`] = `
<div>
  <h1>
     Basic Test
  </h1>
</div>
`;

Isso é tudo que temos para os testes de snapshot, mas, se você ler a seção onde exponho minhas opiniões, perceberá que eu não faço o teste de snapshot. Eu o incluí aqui porque, assim como o Enzyme, eles são bem comuns e são algo que você deveria conhecer, mas explicarei abaixo por que eu não o uso.

Vamos olhar novamente o que é o teste de snapshot. Ele essencialmente permitirá que você veja como o seu componente mudou desde o último teste. Os benefícios disso são:

  • É bem rápido e fácil de implementar e, algumas vezes, requer somente algumas linhas de código.
  • Você pode ver se seu componente está renderizando corretamente e ver os nós do DOM claramente com a função .debug().

Desvantagens e argumentos contra os testes de snapshot:

  • A única coisa que o teste de snapshot faz é dizer a você onde a sintaxe do seu código sofreu alterações desde o último teste.
  • Então, isso é realmente um teste? Alguns diriam que não muito.
  • Renderizar o app corretamente é um trabalho do React. Desse modo, você estaria indo um pouco na direção de testar uma biblioteca de terceiros.
  • Comparar diffs é algo que pode ser feito com o controle de versões do git. Isso não deveria ser um trabalho do teste de snapshot.
  • Um teste que falhou, aqui, não significa que sua aplicação não está funcionando como pretendido. Significa apenas que o seu código sofreu mudanças desde a última vez em que você rodou o teste. Isso poderia levar à vários falsos negativos e gerar uma falha na confiança na testagem. Isso também poderia levar pessoas a apenas atualizar os testes sem observá-los atentamente.
  • Testes de snapshot também dizem se sua sintaxe do JSX está correta. Porém, novamente, isso pode ser feito facilmente em um ambiente de desenvolvimento. Rodar um teste de snapshot apenas para verificar erros de sintaxe não faz nenhum sentido.
  • Pode ser bem difícil de se entender o que está acontecendo em um teste de snapshot, visto que a maioria das pessoas o usam com o teste com shallow(), que não renderiza componentes filhos. Então, ela não dá ao desenvolvedor nenhuma percepção do que está acontecendo realmente.

Veja a seção de leitura complementar para mais informações.

Detalhes de implementação de testes com Enzyme

Aqui eu vou dar a você um exemplo do motivo de não testar detalhes de implementação. Vamos dizer que temos um simples componente contador, como o que mostramos abaixo:

import React, { Component } from 'react';


class Counter extends Component {
  constructor(props) {
    super(props)

    this.state = {
      count: 0
    }
  }

  increment = () => {
    this.setState({count: this.state.count + 1})
  }

  // Este código incorreto ainda fará com que os testes passem
  // <button onClick={this.incremen}>
  //   Clicked: {this.state.count}
  // </button>

  render() {
    return (
      <div>
        <button className="counter-button" onClick={this.incremen}>
          Clicked: {this.state.count}
        </button>
      </div>
  )}
}

export default Counter;

Você notará que eu fiz um comentário sugerindo que um aplicativo não funcional, ainda assim, passaria nos testes. Como exemplo, podemos escrever incorretamente o nome da função no evento onClick.

Vamos ver abaixo nos testes por que isso acontece.

import React from 'react';
import ReactDOM from 'react-dom';
import Counter from '../counter';

import Enzyme, { shallow, render, mount } from 'enzyme';
import toJson from 'enzyme-to-json';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() })

// A atribuição de função incorreta no método onClick 
// ainda fará os testes passarem.

test('o método increment incrementa a contagem', () => {
  const wrapper = mount(<Counter />)

  expect(wrapper.instance().state.count).toBe(0)

  // wrapper.find('button.counter-button').simulate('click')
  // wrapper.setState({count: 1})
  wrapper.instance().increment()
  expect(wrapper.instance().state.count).toBe(1)
})

Rodar o código acima passará nos testes, assim como usar 0 wrapper.setState(). Então, temos testes passando em uma aplicação não funcional. Eu não sei você, mas isso não me transmite a confiança de que nossa aplicação funcionará como pretendido para os nossos usuários finais.

Simular a ação de clique no botão não passará nos testes, mas isso pode gerar um problema oposto: um falso negativo. Vamos dizer que nós queremos aplicar mudanças no estilo do botão, declarando uma nova classe CSS nele – uma situação bem comum. Nossos testes falharão, pois não conseguiremos mais achar o botão. Nossa aplicação, no entanto, continuará funcionando, o que nos dará um falso negativo. Isso também acontece sempre que mudarmos os nomes de nossas funções ou o estado das variáveis.

Sempre que nós quisermos mudar nossas funções e nomes das classes do CSS, teremos que reescrever nossos testes, um processo bem ineficiente e tedioso.

Então, o que podemos fazer para resolver?

react-testing-library

useState

Observando a documentação da biblioteca react-testing-library, podemos perceber que o princípio orientador mais importante é:

Quanto mais seus testes se assemelharem à maneira como seu software é usado, mais confiança você terá (de que sua aplicação funciona adequadamente).

Vamos manter esse princípio orientador em mente conforme seguimos com nossos testes.

Vamos começar com um componente básico de hooks do React e testar seu state e suas propriedades (do inglês, props).

import React, { useState } from 'react';


const TestHook = (props) => {
  const [state, setState] = useState("Initial State")

  const changeState = () => {
    setState("Initial State Changed")
  }

  const changeNameToSteve = () => {
    props.changeName()
  }

  return (
  <div>
    <button onClick={changeState}>
      State Change Button
    </button>
    <p>{state}</p>
    <button onClick={changeNameToSteve}>
       Change Name
    </button>
    <p>{props.name}</p>
  </div>
  )
}


export default TestHook;

Nossas props estão vindo do componente pai

  const App = () => {
      const [state, setState] = useState("Some Text")
      const [name, setName] = useState("Moe")
  ...
      const changeName = () => {
        setName("Steve")
      }

      return (
        <div className="App">
         <Basic />
        <h1> Counter </h1>
         <Counter />
        <h1> Basic Hook useState </h1>
         <TestHook name={name} changeName={changeName}/>
    ...     

Então, mantendo nosso princípio orientador em mente, como os nossos testes ficarão?

A maneira que nosso usuário final usará essa aplicação será: ver algum texto na UI, ver o texto no botão, clicar e finalmente ver um outro texto na UI.

É assim que nós escrevemos nossos testes usando a biblioteca de testes do React.

Use este comando para instalar a biblioteca react-testing-library:

npm install @testing-library/react

Não use este comando:

npm install react-testing-library

Agora, vamos aos testes.

import React from 'react';
import ReactDOM from 'react-dom';
import TestHook from '../test_hook.js';
import {render, fireEvent, cleanup} from '@testing-library/react';
import App from '../../../App'

afterEach(cleanup)

it('Texto no state se altera quando o botão é clicado', () => {
    const { getByText } = render(<TestHook />);

    expect(getByText(/inicial/i).textContent).toBe("State inicial")

    fireEvent.click(getByText("Botão de mudança do state"))

    expect(getByText(/inicial/i).textContent).toBe("State inicial alterado")
 })


it('clique do botão altera as props', () => {
  const { getByText } = render(<App>
                                <TestHook />
                               </App>)

  expect(getByText(/Moe/i).textContent).toBe("Moe")

  fireEvent.click(getByText("Mudar nome"))

  expect(getByText(/Steve/i).textContent).toBe("Steve")
})

Começamos com nossas importações usuais.

Depois, temos a função afterEach(cleanup). Como não estamos usando o teste com shallow(), temos que fazer a desmontagem (em inglês, unmount), ou a limpeza depois de cada teste. É exatamente esse o papel dessa função.

getByText é o método da query que obtivemos desestruturando o valor da função de renderização. Existem muitos mais métodos de query, mas esse você usará a maior parte do tempo.

Para testar nosso state, perceba que nós não estamos nenhum nome de função ou de variável. Estamos mantendo nosso princípio orientador ao não testar detalhes de implementação. Como o usuário final verá o texto no botão da interface, é assim que buscaremos os nós do DOM. Nós também buscaremos o botão dessa maneira e clicaremos nele. Finalmente, buscaremos o estado final, também com base no texto.

(/Inicial/i) é uma expressão regular (do inglês, regex expression) que retorna o primeiro nó que contenha pelo menos o texto "inicial".

Também podemos fazer exatamente a mesma coisa com as props. Se as props forem mudadas em App.js, precisaremos renderizá-las junto com nosso componente. Assim como no exemplo anterior, não estamos testando nomes de função ou variáveis, mas fazemos isso do mesmo jeito que nosso usuário faria ao usar nossa aplicação, ou seja, através do texto que ele vê na interface.

Espero que isso dê a você uma boa ideia de como testar com react-testing-library e seu princípio orientador. Você, geralmente, vai querer usar getByText na maior parte do tempo. Existem poucas exceções e nós as veremos conforme seguimos com a explicação.

useReducer

Agora, nós podemos testar um componente com o hook useReducer. Neste caso, precisaremos de ações e reducers para trabalhar com nosso componente. Então, vamos configurá-los:

Nosso reducer

import * as ACTIONS from './actions'

export const initialState = {
    stateprop1: false,
}

export const Reducer1 = (state = initialState, action) => {
  switch(action.type) {
    case "SUCCESS":
      return {
        ...state,
        stateprop1: true,
      }
    case "FAILURE":
      return {
        ...state,
        stateprop1: false,
      }
    default:
      return state
  }
}

E as ações:




export const SUCCESS = {
  type: 'SUCCESS'
}

export const FAILURE = {
  type: 'FAILURE'
}

Manteremos as coisas simples e usaremos ações ao invés de criadores de ação.

Por fim, o componente usará essas ações e reducers:

import React, { useReducer } from 'react';
import * as ACTIONS from '../store/actions'
import * as Reducer from '../store/reducer'


const TestHookReducer = () => {
  const [reducerState, dispatch] = useReducer(Reducer.Reducer1, Reducer.initialState)

  const dispatchActionSuccess = () => {
    dispatch(ACTIONS.SUCCESS)
  }

  const dispatchActionFailure = () => {
    dispatch(ACTIONS.FAILURE)
  }


  return (
    <div>
       <div>
        {reducerState.stateprop1
           ? <p>stateprop1 is true</p>
           : <p>stateprop1 is false</p>}
       </div>
       <button onClick={dispatchActionSuccess}>
         Dispatch Success
       </button>
    </div>
  )
}


export default TestHookReducer;

Esse é um simples componente que mudará o stateprop1 de falso para verdadeiro ao disparar uma ação de SUCCESS.

Agora, vamos ao nosso teste.

import React from 'react';
import ReactDOM from 'react-dom';
import TestHookReducer from '../test_hook_reducer.js';
import {render, fireEvent, cleanup} from '@testing-library/react';
import * as Reducer from '../../store/reducer';
import * as ACTIONS from '../../store/actions';


afterEach(cleanup)

describe('testar o reducer e as ações', () => {
  it('deve retornar ao state inicial', () => {
    expect(Reducer.initialState).toEqual({ stateprop1: false })
  })

  it('deve alterar stateprop1 de false para true', () => {
    expect(Reducer.Reducer1(Reducer.initialState, ACTIONS.SUCCESS ))
      .toEqual({ stateprop1: true  })
  })
})

it('O reducer altera stateprop1 de false para true', () => {
   const { container, getByText } = render(<TestHookReducer />);

   expect(getByText(/stateprop1 is/i).textContent).toBe("stateprop1 is false")

   fireEvent.click(getByText("Dispatch Success"))

   expect(getByText(/stateprop1 is/i).textContent).toBe("stateprop1 is true")
})

Primeiro, começamos testando nosso reducer e dividimos os testes para o reducer no bloco describe. Esses são testes bem básicos. Estamos usando esses testes para ter certeza de que o state inicial é o que queremos e que as ações produzem o resultado que queremos.

Você pode argumentar que a testagem do reducer é um teste de detalhe de implementação, mas o que eu encontrei na prática foi que a testagem das ações e dos reducers são testes unitários que são sempre necessários.

Esse é um exemplo simples. Então, ele não parece um grande desafio agora, mas, em grandes projetos e aplicações, não fazer a testagem de reducers e ações pode ter consequências desastrosas. Então, ações e reducers seriam uma exceção para a regra sobre não testar detalhes de implementação.

Agora, temos nossos testes para o componente em si. Observe, novamente, que, aqui, não estamos testando detalhes de implementação. Estamos usando o mesmo padrão do exemplo anterior useState, obtendo nossos nós do DOM por texto, além de encontrando e clicando no botão com o texto.

useContext

Agora vamos seguir em frente e testar se um componente filho pode atualizar o state do contexto em um componente pai. Isso pode parecer complexo, mas é bem simples e direto.

Primeiro, precisamos do nosso objeto de contexto, onde inicializaremos nosso próprio arquivo.

import React from 'react';

const Context = React.createContext()

export default Context

Também precisamos atualizar nosso componente app pai, que manterá o provedor (do inglês, provider) de Context. O valor passado para o Provider será o valor do state, além da função setState do componente App.js.

import React, { useState } from 'react';
import TestHookContext from './components/react-testing-lib/test_hook_context';


import Context from './components/store/context';


const App = () => {
  const [state, setState] = useState("Um texto")
  

  const changeText = () => {
    setState("Outro texto")
  }


  return (
    <div className="App">
    <h1> Basic Hook useContext</h1>
     <Context.Provider value={{changeTextProp: changeText,
                               stateProp: state
                                 }} >
        <TestHookContext />
     </Context.Provider>
    </div>
  );
}

export default App;

E para o nosso componente

import React, { useContext } from 'react';

import Context from '../store/context';

const TestHookContext = () => {
  const context = useContext(Context)

  return (
    <div>
    <button onClick={context.changeTextProp}>
        Alterar texto
    </button>
      <p>{context.stateProp}</p>
    </div>
  )
}


export default TestHookContext;

Temos um componente simples, que mostra o texto inicializado em App.js e passamos a função setState para o método onClick.

Observação: o state mudou, inicializado e contido em nosso componente App.js. Simplesmente passamos o valor do state e a função setState para nosso componente filho através do contexto, mas o state foi controlado no componente App.js. Isso será importante para entender nosso teste.

Aqui está o nosso teste:

import React from 'react';
import ReactDOM from 'react-dom';
import TestHookContext from '../test_hook_context.js';
import {act, render, fireEvent, cleanup} from '@testing-library/react';
import App from '../../../App'

import Context from '../../store/context';

afterEach(cleanup)

it('Context value is updated by child component', () => {

   const { container, getByText } = render(<App>
                                            <Context.Provider>
                                             <TestHookContext />
                                            </Context.Provider>
                                           </App>);

   expect(getByText(/Some/i).textContent).toBe("Um texto")

   fireEvent.click(getByText("Alterar texto"))

   expect(getByText(/Some/i).textContent).toBe("Outro texto")
})

Até para o nosso contexto, você pode perceber que não quebramos o nosso padrão de testes. Ainda encontramos e simulamos nossos eventos com os testes.

Fiz a inclusão do <Context.Provider/> e do componente <TestHookContext /> na função de renderização, pois faz com que o código fique mais legível, mas não precisamos deles necessariamente. Nosso teste funcionará se passarmos apenas o componente <App /> para a função de renderização.

const { container, getByText } = render(<App/>) 

Por que esse é o caso?

Vamos pensar de novo no que sabemos sobre o contexto. Todo o state do contexto é controlado em App.js. Por essa razão, é o nosso componente principal que estamos testando realmente, mesmo que pareça que estamos testando o componente filho que usa o hook useContext. Esse código também funciona por causa da criação/montagem do componente (em inglês, mount/render). Como sabemos, no teste com shallow(), os componentes filhos não são renderizados, somente na criação. Sabendo que <Context.Provider /> e <TestHookContext /> são ambos componentes filhos de <App />, eles serão renderizados automaticamente.

Componentes de formulários controlados

Um componente de formulário controlado basicamente significa que o formulário trabalhará através do state do React ao invés do formulário manter seu próprio state. Desse modo, o método onChange salvará o texto do input no React em cada toque no teclado.

Testar o formulário será um pouco diferente do que temos visto até agora, mas vamos tentar manter os nossos princípios guias em mente.

import React, { useState } from 'react';

const HooksForm1 = () => {
  const [valueChange, setValueChange] = useState('')
  const [valueSubmit, setValueSubmit] = useState('')

  const handleChange = (event) => (
    setValueChange(event.target.value)
  );

  const handleSubmit = (event) => {
    event.preventDefault();
    setValueSubmit(event.target.text1.value)
  };

    return (
      <div>
       <h1> Formulário de hooks do React </h1>
        <form data-testid="form" onSubmit={handleSubmit}>
          <label htmlFor="text1">Texto de entrada:</label>
          <input id="text1" onChange={handleChange} type="text" />
          <button type="submit">Enviar</button>
        </form>
        <h3>React State:</h3>
          <p>Alterar: {valueChange}</p>
          <p>Valor de envio: {valueSubmit}</p>
        <br />
      </div>
    )
}


export default HooksForm1;

Temos aqui um formulário básico. Vamos mostrar o valor da mudança e enviar o valor em nosso JSX. Temos o atributo data-testid="form", que usaremos no nosso teste da query para o formulário.

Nossos testes:

import React from 'react';
import ReactDOM from 'react-dom';
import HooksForm1 from '../test_hook_form.js';
import {render, fireEvent, cleanup} from '@testing-library/react';

afterEach(cleanup)

//Teste de um formulário de componente controlado.
it('Inserir texto atualiza o state', () => {
    const { getByText, getByLabelText } = render(<HooksForm1 />);

    expect(getByText(/Change/i).textContent).toBe("Alterar: ")

    fireEvent.change(getByLabelText("Texto de entrada:"), {target: {value: 'Text' } } )

    expect(getByText(/Change/i).textContent).not.toBe("Alterar: ")
 })


 it('O envio do formulário funciona corretamente', () => {
     const { getByTestId, getByText } = render(<HooksForm1 />);

     expect(getByText(/Submit Value/i).textContent).toBe("Valor de envio: ")

     fireEvent.submit(getByTestId("form"), {target: {text1: {value: 'Texto' } } })

     expect(getByText(/Submit Value/i).textContent).not.toBe("Valor de envio: ")
  })

Considerando que um elemento de input vazio não possui texto, usaremos a função getByLabelText() para obter o nó do input. Ainda manteremos nosso princípio guia, visto que o texto da label é o que o usuário lerá antes de digitar o texto.

Note que disparará o evento .change() ao invés do evento usual, .click(). Também passaremos um dado teste como exemplificado abaixo:

{ target: { value: "Texto" } }

Considerando que o valor do formulário será acessado na forma de event.target.value, é assim que simularemos o evento.

Geralmente, não sabemos qual texto o usuário enviará, então podemos simplesmente usar a palavra-chave .not para ter certeza de que o texto mudou ou renderizou o método.

Podemos testar o envio do formulário de modo similar. A única diferença é que usamos o evento .submit() e passamos dados de teste dessa maneira:

{ target: { text1: { value: 'Texto' } } }

É assim que acessamos dados do formulário através do evento sintético quando um usuário o submeter, onde text1 é o id de nosso elemento input. Temos que fugir um pouco do nosso padrão aqui e usar o atributo data-testid="form" para buscar o formulário, visto que não há outro modo de fazê-lo.

É isso que temos para o formulário. Não é tão diferente de outros exemplos já feitos por nós anteriormente. Se você achar que já entendeu tudo, podemos seguir para algo um pouco mais complexo.

useEffect e requisições a APIs com o axios

Agora, vamos ver como testaríamos o hook useEffect e as requisições a APIs. Isso será um pouco diferente do que temos visto até agora.

Digamos que temos um url passado de um componente pai para um filho.


...

     <TestAxios url='https://jsonplaceholder.typicode.com/posts/1' />
     
 ... 

O componente seria algo assim:

import React, { useState, useEffect } from 'react';
import axios from 'axios';


const TestAxios = (props) => {
  const [state, setState] = useState()

  useEffect(() => {
    axios.get(props.url)
      .then(res => setState(res.data))
  }, [])


  return (
    <div>
    <h1> Teste do axios </h1>
        {state
          ? <p data-testid="title">{state.title}</p>
          : <p>...Carregando</p>}
    </div>
  )
}


export default TestAxios;

Simplesmente fazemos uma requisição a uma API e salvamos os resultados em um state local. Também usamos uma expressão ternária no nosso método de renderização para esperar até que a requisição esteja completa. Assim, mostrarmos o título do json como placeholder.

Você notará que, novamente, por necessidade, teremos que usar o atributo data-testid. Novamente, é um detalhe de implementação, já que o usuário não verá nem vai interagir com esse atributo de nenhuma maneira, mas, aqui, é mais realista, uma vez que, geralmente, não saberemos o texto vindo da requisição de antemão.

Usaremos dados mockados (de simulação) nesse teste.

Um mock é uma maneira de simular um comportamento que não queremos que aconteça em nossos testes. Por exemplo, fazemos o mock das requisições à API porque não queremos fazer requisições reais em nossos testes.

Não queremos requisições reais em nossos testes por diversas razões: farão com que nossos testes fiquem mais lentos, podem nos dar um falso negativo, as requisições à API podem nos custar dinheiro ou podemos alterar nosso banco de dados com os testes.

import React from 'react';
import ReactDOM from 'react-dom';
import TestAxios from '../test_axios.js';
import {act, render, fireEvent, cleanup, waitForElement} from '@testing-library/react';

import axiosMock from "axios";

Temos nossas importações usuais, mas você notará algo específico. Estamos importando axiosMock da biblioteca axios. Não estamos importando um objeto axios da biblioteca axios. Na verdade, estamos mockando (simulando) a biblioteca axios em si.

Como?

Usando a funcionalidade de mocking oferecida pelo Jest.

Primeiro, vamos fazer um diretório __mocks__ adjacente para testar nosso diretório teste. Seria algo assim:

image-13

Dentro do diretório de mocks, temos um arquivo axios.js. Essa é a nossa biblioteca axios falsa. Dentro de nossa biblioteca axios falsa, temos nossa função de mock do jest.

Funções de mock nos permitem usá-las em nosso ambiente do Jest sem ter que realmente implementar sua lógica.

Então, basicamente, não vamos implementar a lógica através de uma requisição do axios. Usaremos a função mock em vez disso.

export default {
  get: jest.fn(() => Promise.resolve({ data: {} }) )
};

Aqui, temos nossa função falsa get. Ela é uma função simples que, na verdade, é um objeto em JS. get é nossa chave e o valor é a função mock. Como em uma requisição a APIs do axios, resolvemos uma promise (em português, promessa). Não passaremos nenhum dado aqui. Faremos isso em nossa configuração dos testes.

Segue aqui a nossa configuração de testes:

//importações
...

afterEach(cleanup)

it('A solicitação assíncrona do axios funciona', async () => {
  axiosMock.get.mockResolvedValue({data: { title: 'some title' } })

  const url = 'https://jsonplaceholder.typicode.com/posts/1'
  const { getByText, getByTestId, rerender } = render(<TestAxios url={url} />);

  expect(getByText(/...Loading/i).textContent).toBe("...Carregando")

  const resolvedEl = await waitForElement(() => getByTestId("title"));

  expect((resolvedEl).textContent).toBe("some title")

  expect(axiosMock.get).toHaveBeenCalledTimes(1);
  expect(axiosMock.get).toHaveBeenCalledWith(url);
 })

A primeira coisa que fazemos em nosso teste é chamar nossa requisição get do axios e fazer o mock do valor resolvido com a função, ironicamente chamada de mockResolvedValue, provida pelo Jest. Essa função faz exatamente o que seu nome diz, ela resolve uma promise com o dado que passamos nela, simulando o que o axios faz.

Essa função tem que ser chamada antes da nossa função render(). Caso contrário, nosso teste não funcionará. Lembre-se de que estamos mockando a biblioteca axios em si. Quando nosso componente rodar o comando import axios from 'axios'; estará importando nossa biblioteca falsa do axios ao invés da verdadeira e essa biblioteca falsa será substituída em nosso componente toda vez que usarmos o axios.

A seguir, temos o nosso nó de texto "...Carregando", visto que ele é mostrado antes de a promise ser resolvida. Depois disso, temos uma função que ainda não vimos até agora, a função waitForElement(), que esperará até que a promise seja resolvida antes de seguir para a próxima afirmação.

Também note que as palavras-chave await e async são usadas do mesmo jeito que são usadas fora do ambiente de teste.

Uma vez resolvida, o nó do DOM terá o texto "some title", que é o dado passado para nossa biblioteca falsa do axios.

A seguir, temos a certeza de que a requisição foi chamada apenas uma vez e com o url certo. Mesmo que estejamos testando o url, não fazemos uma requisição à API com esse url.

Ficamos por aqui com nossas requisições à API com axios. Na próxima sessão, exploraremos um pouco de testes com o Cypress.

Cypress

Agora, vamos explorar um pouco o Cypress, que eu acredito ser o melhor framework para rodar testes de ponta a ponta. Nos afastaremos um pouco do Jest agora. Vamos trabalhar unicamente com o Cypress, que possui seu próprio ambiente e sintaxe de testes.

O Cypress é incrível e poderoso. De fato, ele é tão incrível e poderoso que podemos rodar todos os testes que já vimos até agora em apenas um bloco de código de teste e observar o Cypress rodar esses testes em tempo real em um simulador de navegador.

Muito legal, não é?

Eu penso que sim. De qualquer forma, antes de fazermos tudo isso, precisamos configurar o Cypress. Surpreendentemente, o Cypress pode ser instalado como um módulo normal do npm.

npm install cypress

Para ter acesso ao Cypress, você precisa rodar este comando no seu terminal.

node_modules/.bin/cypress open

Se escrever esse comando toda vez parecer muito incômodo, você pode adicioná-lo no seu arquivo package.json.

...

  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "cypress": "node_modules/.bin/cypress open", 
    
   ...

Isso permite, então, que você abra o Cypress apenas usando o seguinte comando: npm run cypress.

Ao abrir o Cypress, você encontrará uma interface gráfica (em inglês, GUI, ou Graphic User Interface), que pode ter a aparência seguinte:

image-14

Para rodar os testes no Cypress, sua aplicação deverá estar rodando ao mesmo tempo, o que veremos em um segundo.

Rodar o comando cypress open dará a você uma configuração básica do Cypress e criará alguns arquivos e diretórios para você automaticamente. Um diretório será criado na raiz do seu projeto. Escreveremos nosso código no diretório integration.

Começaremos excluindo o diretório examples. Diferentemente do Jest, arquivos do Cypress tem como extensão .spec.js.  Por ser um teste de ponta a ponta, rodaremos no nosso arquivo App.js. Sua estrutura de diretórios, agora, deve se parecer com a da imagem abaixo:

image-21

Também configuramos um url base no arquivo cypress.json, como no exemplo abaixo:

{ "baseUrl": "http://localhost:3000" }

Agora, vamos ao nosso longo teste monolítico

import React from 'react';

describe ('Teste completo de ponta a ponta', () => {
  it('teste completo de ponta a ponta', () => {
    cy.visit('/')
    //teste de contador
    cy.contains("Clicked: 0")
      .click()
    cy.contains("Clicked: 1")
    // teste básico de hooks
    cy.contains("State inicial")
    cy.contains("Botão de alteração do state")
      .click()
    cy.contains("State inicial alterado")
    cy.contains("Moe")
    cy.contains("Alterar nome")
      .click()
    cy.contains("Steve")
    //teste de useReducer
    cy.contains('stateprop1 is false')
    cy.contains('Dispatch Success')
      .click()
    cy.contains('stateprop1 is true')
    //teste de useContext
    cy.contains("Um texto")
    cy.contains('Alterar texto')
      .click()
    cy.contains("Outro texto")
    //teste do formulário
    cy.get('#text1')
      .type('Novo texto {enter}')
    cy.contains("Alterara: Novo texto")
    cy.contains("Enviar valor: Novo texto")
    //teste de axios
    cy.request('https://jsonplaceholder.typicode.com/posts/1')
      .should(res => {
          expect(res.body).not.to.be.null
          cy.contains(res.body.title)
        })
  });
});

Como mencionado antes, estamos rodando todos os testes que já passamos em apenas um bloco de código. Separei cada sessão com um comentário para que fique mais fácil a visualização.

Nosso teste pode ser um pouco intimidador a princípio, mas a maioria dos testes individuais seguirão o mesmo padrão arranjo-ação-declaração.


cy.contains(Algum texto de innerHTML do nó do DOM)

cy.contains (texto do botão)
.click()

cy.contains(Texto atualizado de innerHTML do nó do DOM)

Considerando que esse é um teste de ponta a ponta, você não encontrará qualquer tipo de mock. Nossa aplicação será rodada na sua versão de desenvolvimento completa em um simulador de navegador com uma interface. Essa é a maneira de testagem mais próxima da realidade que podemos ter.

Ao contrário dos testes unitários e de integração, não precisamos explicitamente declarar algumas coisas. Isso acontece porque o Cypress tem alguns comandos embutidos em algumas declarações por padrão. Declarações padrão são exatamente como o nome as descreve. Elas são declaradas por padrão. Então, não há a necessidade de adicionar um combinador.

Declarações padrão do Cypress (documentação do Cypress, em inglês)

Os comandos são encadeados uns nos outros. Então, ordená-los é bem importante. Cada comando esperará até que o comando anterior esteja completo antes de rodar.

Mesmo nas testagens com Cypress, ainda manteremos nossa filosofia de não testar detalhes de implementação. Na prática, isso quer dizer que não usaremos classes de html/css, ids ou propriedades como seletores se pudermos. A única vez em que precisaremos usar ids é para obter acesso ao elemento de input.

Faremos uso do comando cy.contains(), que retornará um nó do DOM com um texto correspondente. O que nosso usuário final fará será ver o texto na interface e interagir com ele. Então, testar dessa maneira será nossa linha com nossos princípios-guia.

Como não estamos mockando nada, você perceberá que nossos testes serão bem simples. Isso é bom. Já que essa é uma aplicação rodando, nossos testes não terão nenhum valor artificial.

Em nossos testes com axios, faremos uma requisição de http real para nosso endpoint. Fazer uma requisição em um teste de ponta a ponta é bem comum. Então, verificaremos se o valor não é nulo. Depois disso, nos certificaremos de que nosso dado aparece na interface.

Se for feito corretamente, você verá que o cypress rodou com sucesso os testes no Chromium.

image-22

Integração contínua

Rastrear e rodar todos esses testes manualmente pode se tornar uma tarefa um pouco tediosa. Para resolver isso, temos a integração contínua (em inglês, Continuous Integration, ou CI), que é uma maneira de automatizar nossos testes continuamente.

Travis CI

Para deixar as coisas mais simples, usaremos o Travis CI para nossa integração contínua. Você deve ter em mente, porém, que existem configurações muito mais complexas para a integração contínua usando o Docker e o Jenkins.

Você precisará estar conectado a uma conta do Travis e a uma do Github. Por sorte, ambas são gratuitas.

Eu sugiro que você selecione a opção "Entrar com Github" no Travis CI.

Ao sincronizar sua conta, você pode ir até o ícone que leva até o seu perfil e clicar no botão de ativar/desativar, correspondente ao repositório no qual você quer ativar a CI.

image-15-1

Aqui, o Travis CI sabe do que você precisa para configurar um arquivo .travis.yml na raiz do seu projeto.

language: node_js

node_js: 
  - stable
  
  
install:
  - npm install

script:
  - npm run test
  - npm run coveralls

Isso, essencialmente, diz ao Travis que estamos usando o node_js. Faça o download de uma versão estável, instale as dependências e rode os comandos npm run test e npm run coveralls.

É isso. Você pode ir ao painel de controle e começar a build. O Travis rodará os testes automaticamente e entregará a você um resultado como o que vemos abaixo. Se seus testes passarem, você pode seguir em frente. Se falharem, sua build falhará e você precisará consertar o seu código e reiniciar a build.

image-16

Testes de cobertura

Os testes de cobertura nos dão um relatório que, basicamente, nos diz o quanto de nosso código está sendo testado.

Você precisará se registrar e sincronizar com sua conta do GitHub. Do mesmo modo que fizemos com o Travis CI, é necessário apenas que você vá para a aba Add Repo (Adicionar repositórios, em português) e ative o repositório que você também ativou no Travis CI.

image-17

Agora, vá em seu arquivo package.json e adicione a linha de código de coveralls:

  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --coverage",
    "eject": "react-scripts eject",
    "cypress": "node_modules/.bin/cypress open", 
    "coveralls": "cat ./coverage/lcov.info | node node_modules/.bin/coveralls"
  },

Certifique-se de que a flag --coverage esteja adicionada ao comando react-scripts test. Isso é o que vai gerar os dados necessários de que o teste de cobertura precisará para gerar o relatório final.

Você pode, de fato, ver esses dados de cobertura no Travis CI depois que seus testes terminarem de rodar.

image-18

Considerando que não estamos lidando com um repositório privado nem com o Travis CI Pro, não precisamos nos preocupar com nenhum tipo de chave.

Uma vez concluído, você poderá adicionar uma insígnia (em inglês, badge) ao seu README, copiando o link gerado no dashboard.

image-19

O resultado será mais ou menos assim:

image-20

Conclusão

Você pode se considerar nos melhores 20% em termos de desenvolvedores com relação à testagem em React caso tenha conseguido acompanhar o tutorial até aqui.

Obrigado pela leitura, pessoal.

Siga o autor no Twitter para ver mais tutoriais escritos por ele.

Leitura complementar

Publicações em blogs (em inglês)

https://djangostars.com/blog/what-and-how-to-test-with-enzyme-and-jest-full-instruction-on-react-component-testing/

https://engineering.ezcater.com/the-case-against-react-snapshot-testing

https://medium.com/@tomgold_48918/why-i-stopped-using-snapshot-testing-with-jest-3279fe41ffb2

https://circleci.com/blog/continuously-testing-react-applications-with-jest-and-enzyme/

https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html

https://willowtreeapps.com/ideas/best-practices-for-unit-testing-with-a-react-redux-approach

https://blog.pragmatists.com/genuine-guide-to-testing-react-redux-applications-6f3265c11f63

https://hacks.mozilla.org/2018/04/testing-strategies-for-react-and-redux/

https://codeburst.io/deliberate-practice-what-i-learned-from-reading-redux-mock-store-8d2d79a4b24d

https://www.robinwieruch.de/react-testing-tutorial/

https://medium.com/@ryandrewjohnson/unit-testing-components-using-reacts-new-context-api-4a5219f4b3fe

Textos de Kent C. Dodds sobre testes (em inglês)

https://kentcdodds.com/blog/introducing-the-react-testing-library

https://kentcdodds.com/blog/unit-vs-integration-vs-e2e-tests

https://kentcdodds.com/blog/why-i-never-use-shallow-rendering

https://kentcdodds.com/blog/demystifying-testing

https://kentcdodds.com/blog/effective-snapshot-testing

https://kentcdodds.com/blog/testing-implementation-details

https://kentcdodds.com/blog/common-testing-mistakes

https://kentcdodds.com/blog/ui-testing-myths

https://kentcdodds.com/blog/why-youve-been-bad-about-testing

https://kentcdodds.com/blog/the-merits-of-mocking

https://kentcdodds.com/blog/how-to-know-what-to-test

https://kentcdodds.com/blog/avoid-the-test-user

Atalhos úteis/threads do GitHub (em inglês)

https://devhints.io/enzyme

https://devhints.io/jest

https://github.com/ReactTraining/react-router/tree/master/packages/react-router/modules/__tests__

https://github.com/airbnb/enzyme/issues/1938

https://gist.github.com/fokusferit/e4558d384e4e9cab95d04e5f35d4f913

https://airbnb.io/enzyme/docs/api/selector.html

Documentação

https://docs.cypress.io

https://airbnb.io/enzyme/

https://github.com/dmitry-zaets/redux-mock-store

https://jestjs.io/docs/en

https://testing-library.com/docs/learning

https://sinonjs.org/releases/v7.3.2/

https://redux.js.org/recipes/writing-tests

https://jestjs.io/docs/en/using-matchers

https://jestjs.io/docs/en/api