Testando na prática seus componentes React usando o Jest

Alexandre Magno Teles
Training Center
Published in
8 min readSep 13, 2018

Uma das maiores vantagens do React, é, sem dúvida, utilizar o Enzyme (feito pelo Airbnb) para testar componentes tirando todo proveito do virtual DOM. Também podemos explorar as ferramentas poderosas do Jest como teste com snapshot e mocks para módulos externos. Porém, no caso do Jest, não é exclusividade somente para o React, embora sua popularidade tenha crescido na mesma época em que o React se tornou o framework da vez e ele combina bastante com este paradigma.

Já falei aqui sobre como trabalhar com os diferentes padrões de componentes para react, que logo em seguida, vem a questão: como testar de uma forma útil e eficiente estes componentes?

Estes testes podem fazer também parte da sua integração contínua, no processo de trabalho e na validação de pronto, caso você queira validá-lo em diferentes ambientes.

Vou mostrar um exemplo prático de um componente que foi isolado para simular um teste de um preenchimento de formulário com opção de pagamento no cartão de crédito, e como conseguimos testar este componente sem precisar ir na interface.

Além disto, vou mostrar como integrá-lo como parte do build no Circleci, rodando os testes em um servidor remoto quando um push para o repositório é realizado.

Aqui temos o componente que será testado:

Componente de Checkout

Como boa prática, construímos um componente que chamamos de Checkout, onde teremos o preenchimento dos dados do cartão de crédito, informações do usuário e envio.

Este componente contém estados (state), propriedades (props) que tratam das diferentes interações no preenchimento dos campos do formulário por um usuário.

É possível testá-lo de forma unitária e ainda simular os eventos e validação e todas as ações que acontecem internamente ao componente. Podemos saber dos estados, propriedades, além de poder configurar estados diferentes e ver o que acontece no componente, podendo simular vários cenários de testes. Isto não é incrível?

Temos aqui o código base do componente de Checkout que vamos testar:

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { injectStripe } from 'react-stripe-elements'

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

this.handleSubmit = this.handleSubmit.bind(this)
this.onChange = this.onChange.bind(this)

this.state = {
email: null,
fullname: null,
authenticated: false,
userId: null,
error: {
fullname: false,
email: false,
payment: false,
message: 'loading'
},
paymentRequested: false
}
}

handleSubmit (ev) {
this.props.stripe
.createToken({ name: this.state.fullname })
.then(({ token }) => {
// do something with the token
})
.catch(e => {
// eslint-disable-next-line no-console
console.log('error to create token')
// eslint-disable-next-line no-console
console.log(e)
this.props.addNotification('Erro no pagamento')
this.setState({
paymentRequested: false
})
})

// However, this line of code will do the same thing:
// this.props.stripe.createToken({type: 'card', name: 'Jenny Rosen'});
}

onChange (ev) {
ev.preventDefault()
let formData = {}
formData[ev.target.name] = ev.target.value
this.setState(formData)
this.setState({ paymentRequested: false })
}

componentDidMount () {
const { user } = this.props

if (user && user.id) {
this.setState({
authenticated: true,
fullname: user.name,
email: user.email,
userId: user.id
})
}
}

render () {
const logged = this.state.authenticated
const { user } = this.props

return (
<form
onSubmit={ this.handleSubmit }
onChange={ this.onChange }
style={ { marginTop: 20 } }
></form>
)
}
}

CheckoutForm.propTypes = {
stripe: PropTypes.object,
onPayment: PropTypes.func,
task: PropTypes.any,
onClose: PropTypes.func,
addNotification: PropTypes.func,
itemPrice: PropTypes.any,
user: PropTypes.object
}

export const CheckoutFormPure = CheckoutForm
export default injectStripe(CheckoutForm)

Quer ver o componente completo? Temos aqui o código fonte.

Temos aqui um componente React que tem os estados referente aos campos do formulário, que muda de acordo com eventos de mudança nos campos. Isto quer dizer que preenchemos os estados quando o usuário digita os dados, como é bastante usado em aplicações React.

Como este componente tem dependência de outro componente, neste caso o Stripe, eu resolvi fazer uma modificação que torna mais fácil testá-lo, pois não quero testar o fluxo do pagamento completo, apenas certificar que o componente grava os dados do formulário para ser enviado posteriormente.

Neste caso eu exportei o componente puro e conectei o componente ao Redux em uma outra parte, assim ele está exposto como módulo disponível para ambos.

Ambiente necessário para rodar o Jest com o React

Para que seu projeto reconheça o React e consiga processar o Javascript escrito nas diferentes variações do Javascript como ES5, você precisa adicionar uma configuração do Babel:

{
"presets": ["es2015", "react"]
}

O que queremos testar?

Para realizar os testes de componentes, seria ideal fazê-lo antes de qualquer código, mas no mundo real muitas vezes desenvolvemos sem teste e depois temos que correr atrás do prejuízo. Isto por que quando desenvolvemos com o TDD realizamos o design do projeto de acordo com os testes e isto muitas vezes afeta toda a arquitetura. Quando não pensamos nos testes, criamos componentes dependentes e que descobrimos o quanto nosso código não estava eficiente quando temos de testar. Por isto, é importante que o teste pelo menos mínimo faça parte do desenvolvimento.

No caso aqui, temos um caso de um teste que será adicionado posteriormente, e que foi útil cobri-lo com teste de qualquer forma. Cobrimos com um teste para conseguir testar uma parte que muitas vezes temos bugs.

Adicionando testes e evoluindo o código

Neste exemplo dos testes que realizamos, usamos o enzyme com o componente mount, que cria toda estrutura de um componente react e uma API que é possível realizar diversas ações, como mudar um state e aplicar eventos, como fizemos a seguir com o simulate.

import React from 'react'
import { CheckoutFormPure } from '../../src/components/checkout/checkout-form'
import { mount, configure } from 'enzyme'
import Adapter from 'enzyme-adapter-react-15'

configure({ adapter: new Adapter() })

describe('components', () => {
describe('checkout component', () => {
it('should start a new checkout form with empty state', () => {
const component = mount()

expect(component).toEqual({})
expect(component.state().fullname).toEqual(null)
expect(component.state().email).toEqual(null)
component.unmount()
})

it('should start a new checkout and set state', () => {
const component = mount()
component.setState({ fullname: 'foo', email: 'mail@example.com' })
expect(component).toEqual({})
expect(component.state().fullname).toEqual('foo')
expect(component.state().email).toEqual('mail@example.com')
component.unmount()
})

it('should start a new checkout and check if a payment is requested and change state', () => {
const component = mount()
component.find('input').first().simulate('change', {
target: {
name: 'fullname',
value: 'Foo me'
}
})
component.find('form').simulate('submit')
expect(component).toEqual({})
expect(component.state().paymentRequested).toEqual(true)
expect(component.state().fullname).toEqual('Foo me')
component.unmount()
})

it('should set the username and email from a logged user', () => {
const component = mount()
expect(component).toEqual({})
expect(component.state().fullname).toEqual('Foo me')
expect(component.state().email).toEqual('foo@mail.com')
component.unmount()
})
})
})

Você pode ver o código fonte aqui.

Fizemos alguns testes em que alteramos o state interno do componente, e verificamos se esta mudança altera o estado de fato, um bom jeito de começar a testar o básico, pois sempre bom seguir baby steps.

Depois adicionamos uma simulação de evento, alterando os campos do formulário e vendo se ele vai alterar o estado como esperado. Com isto temos um teste prático e a partir daí realizar várias ações e continuar com os testes do componente, buscando sempre ter o mínimo de dependência possível, e como explicado neste artigo sobre padrões de componentes react que falei anteriormente, temos que separar os componentes em componentes puros e componentes conectados.

Integrando ao build

Com o processo de integração contínua conseguimos rodar estes testes como parte da integração do projeto e do lançamento das versões do software, validando em diferentes ambientes antes de ir para produção e assegurando de que ele irá passar em todos estes testes antes de ser lançado.

Um servidor de integração contínua é um termo bastante comum na infraestrutura de desenvolvimento, ou devOps em que equipes conseguem trabalhar em um projeto realizando novos releases a cada código integrado, passando por diferentes testes e estágios automatizados

E com esta configuração no Circleci você consegue integrar e rodar estes no seu processo de build e release.

# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#
version: 2
jobs:

build:
docker:
# specify the version you desire here
- image: circleci/node:8.6.0
environment:
PG_HOST: 127.0.0.1
POSTGRES_DB: gitpay_test
POSTGRES_USER: postgres
PG_PASSWD: postgres
NODE_ENV: test
- image: circleci/postgres:9.5
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: gitpay_test

# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
# - image: circleci/mongo:3.4.4

working_directory: ~/repo

steps:
- checkout
- run:
name: install
command: 'curl -k -O -L https://npmjs.org/install.sh | sh'
- restore_cache:
key: dependency-cache-{{ checksum "package.json" }}
- run:
name: Install packages
command: npm install
- save_cache:
key: dependency-cache-{{ checksum "package.json" }}
paths:
- node_modules
#- run: sudo apt install postgresql-client
- run:
name: install dockerize
command: wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
environment:
DOCKERIZE_VERSION: v0.3.0
- run:
name: Wait for db
command: dockerize -wait tcp://localhost:5432 -timeout 1m
#- run:
# name: Set up database
# command: psql -h localhost -p 5432 -c 'create database gitpay_test' -U postgres
- run:
name: Migrate
command: npm run migrate-test
# run tests!
- run:
name: run node tests
command: npm run test
- run:
name: run frontend tests
command: cd frontend && npm run test

workflows:
version: 2
build-all:
jobs:
- build:
filters:
branches:
ignore: gh-pages

Note que é um processo de build para rodar um projeto Node com react no frontend.

Se você quiser saber mais sobre as importância de se trabalhar com builds em projetos eu falo melhor aqui sobre como trabalhar com o Workflow do git para times remotos.

O que aprendemos?

Aprendemos aqui como criar testes de componentes react na prática, controlando os diferentes estados e verificando se temos o resultado esperado. Este foi um exemplo de um projeto já iniciando, mostrando como podemos integrar os testes do React posteriormente em um projeto com o Jest de uma forma fácil e realizar testes essenciais sem ter que modificar todo o projeto ou começar do zero. Também conseguimos fazer que este processo seja realizado no CI (integração contínua) para que o teste seja realizado em qualquer integração do código, e com isto permitir que quando outros enviarem modificações, os testes garantam que o componente ainda tem o seu funcionamento básico.

Espero que tenha ajudado e tenha sido um aprendizado de como utilizar os testes de componentes do React de um jeito direto e com um componente real onde você possa entender melhor e na prática.

Acompanhe o Pull Request onde estes testes foram implementados: https://github.com/worknenjoy/gitpay/pull/160/files

Quer aprender React na prática?

Com o Gitpay você pode trabalhar contribuindo com um projeto open source construído com o React. Veja nossas issues, cadastre-se na plataforma e veja as tarefas disponíveis para começar a criar soluções com o React , aprender e ser recompensado!

Como trabalhar em uma tarefa do Gitpay e aprender na prática.

Fontes:

--

--

Alexandre Magno Teles
Training Center

Citizen Scientist and Software Engineer, looking for the limits beyond mind and machine exploring the world