Teste unitário com React, Jest e Enzyme

Hoje é dia de falar um pouco sobre os testes e sua importância quando o assunto é poupar tempo e evitar dores de cabeça com correções no futuro (inclusive, quando o projeto já estiver em produção!).

Tipos de teste

Existem diferentes tipos de teste, alguns deles voltados para o desenvolvimento de software, como verificar se um bug foi corrigido ou saber se um sistema funciona da maneira que foi definido nas especificações (teste de aceitação). Já outros visam otimizar uma estratégia de marketing, como é o caso do teste A/B, onde são criadas duas versões de um anúncio, email, Landing Page etc. para saber qual tem melhor aceitação por parte do público-alvo.

A importância dos casos teste

Você como programador deve estar se perguntando: “Essa é boa, agora tenho que ficar testando tudo… para onde vai minha produtividade desse jeito?”

Pode parecer perda de tempo, mas na verdade é apenas outra maneira de abordar o problema, pois você evita a necessidade de ficar depurando o código a todo momento para encontrar falhas. Claro que se você não está acostumado, isso exigirá um período de adaptação.

Teste unitário

Neste artigo nos limitaremos aos testes unitários, que como próprio nome já diz, se baseia em testar unidades, ou seja, validar a menor parte testável de um programa, geralmente uma classe ou método. Para fazer essa validação analisamos o comportamento desta unidade passando um valor como entrada e verificando se o valor de saída é o esperado.

O que é Jest?

Jest é um framework de testes open-source em JavaScript, mantido pelo Facebook. Seu intuito é ser simples e eficiente em termos de teste unitário.

E esse tal Enzyme?

Enzyme é um utilitário de teste criado pela Airbnb para tornar mais fácil testar a saída dos componentes do React.

Com ele em vez de testar a árvore de componentes completa podemos testar apenas a saída do componente. Qualquer um dos filhos deste componente não será processado. Isso é chamado de renderização superficial (ou shallow rendering).

Criando um projeto

Como de costume vamos criar um projeto simples no Visual Studio Code. Relembraremos as aulas da tia Cotinha do ensino médio fazendo conversões de unidades das temperaturas Kelvin, Fahrenheit e Celsius e criando casos de teste para essas conversões.

A princípio crie um novo projeto chamado temp-converter com Create React App, já fizemos algo parecido no artigo anterior que falava sobre gráfico de barras com React e D3. Se você não se lembra aqui vão os comandos:

npm install –g create-react-app

create-react-app temp-converter

Agora em src crie um diretório chamado components e em App.js altere o HTML para:

import React, { Component } from 'react';
import './App.css';
import TempConverter from './components/TempConverter';

class App extends Component {
  render() {
    return (
      <div>
        <h2>Conversão de Temperatura</h2>
        <TempConverter />
      </div>
    );
  }
}

export default App;

Em seguida, no diretório components, adicione três arquivos TempConverter.js, TempConverter.test.js e TempConverter.css.

Em TempConverter.js copie e cole o código abaixo:

import React, { Component } from 'react';
import './TempConverter.css';

export default class TempConverter extends Component {
    constructor() {
        super();
        this.state = { tempA: 0, unitA: 'Celsius', unitB: 'Celsius' }
    }

    // Chamado após a atualização do componente
    componentDidUpdate() {
        this.converterTemperature();
    }

    // Converte a unidade de medida da temperatura A para unidade da temperatura B
    converterTemperature() {
        if (this.state.tempA) {
            var tempB = document.getElementById("tempB");

            // Celsius para Fahrenheit ou Kelvin
            if (this.state.unitA === "Celsius") {
                if (this.state.unitB === "Fahrenheit") {
                    tempB.value = this.celsiusToFahrenheit(this.state.tempA);
                }
                else if (this.state.unitB === "Kelvin") {
                    tempB.value = this.celsiusToKelvin(this.state.tempA);
                }
                else {
                    tempB.value = this.state.tempA;
                }
            }

            // Fahrenheit para Celsius ou Kelvin
            if (this.state.unitA === "Fahrenheit") {
                if (this.state.unitB === "Celsius") {
                    tempB.value = this.fahrenheitToCelsius(this.state.tempA);
                }
                else if (this.state.unitB === "Kelvin") {
                    tempB.value = this.fahrenheitToKelvin(this.state.tempA);
                }
                else {
                    tempB.value = this.state.tempA;
                }
            }

            // Kelvin para Celsius ou Fahrenheit
            if (this.state.unitA === "Kelvin") {
                if (this.state.unitB === "Celsius") {
                    tempB.value = this.kelvinToCelsius(this.state.tempA);
                }
                else if (this.state.unitB === "Fahrenheit") {
                    tempB.value = this.kelvinToFahrenheit(this.state.tempA);
                }
                else {
                    tempB.value = this.state.tempA;
                }
            }
        }
    }

    // Conversões ------------------------------------------------
    
    celsiusToFahrenheit(value) {
        return Math.round((value * 1.8 + 32) * 100) / 100;
    }

    celsiusToKelvin(value) {
        return Math.round((value + 273.15) * 100) / 100;
    }

    fahrenheitToCelsius(value) {
        return Math.round(((value - 32) * 5 / 9) * 100) / 100;
    }

    fahrenheitToKelvin(value) {
        return Math.round(((value + 459.67) * 5 / 9) * 100) / 100;
    }

    kelvinToCelsius(value) {
        return Math.round((value - 273.15) * 100) / 100;
    }

    kelvinToFahrenheit(value) {
        return Math.round((value * 1.8 - 459.67) * 100) / 100;
    }

    // -------------------------------------------------------------

    // Renderiza o componente
    render() {
        return (
            <div id="container">
                <div id="blockA">
                    <input type="number" id="tempA" name="tempA" onChange={(e) => this.setState({ tempA: !isNaN(e.target.value) ? parseFloat(e.target.value) : 0 })} />
                    <select id="unitA" name="unitA" onChange={(e) => this.setState({ unitA: e.target.value })}>
                        <option value="Celsius">Grau Celsius</option>
                        <option value="Fahrenheit">Grau Fahrenheit</option>
                        <option value="Kelvin">Kelvin</option>
                    </select>
                </div>
                <span name="unit2" id="equal">=</span>
                <div id="blockB">
                    <input type="number" id="tempB" disabled="disabled" name="tempB" />
                    <select id="unitB" name="unitB" onChange={(e) => this.setState({ unitB: e.target.value })}>
                        <option value="Celsius">Grau Celsius</option>
                        <option value="Fahrenheit">Grau Fahrenheit</option>
                        <option value="Kelvin">Kelvin</option>
                    </select>
                </div>
            </div>
        );
    }
}

Basicamente o que temos:

  1. Alguns states para nos auxiliar durante a troca de unidades de medida;
  2. Uma função chamada converterTemperature que fará o trabalho de pegar a unidade de medida da temperatura A e converter para unidade da temperatura B de acordo com os valores selecionados no campo select;
  3. As conversões que serão realizadas com as chamadas das funções logo abaixo (celsiusToFahrenheit, celsiusToKelvin, fahrenheitToCelsius e assim por diante). Repare que todas as conversões possuem um Math.round((…) * 100) / 100, isso irá garantir um valor numérico com duas casas decimais de precisão;
  4. Uma função render para exibir nosso componente na tela.

Antes de visualizarmos o resultado, adicione o seguinte código CSS no arquivo TempConverter.css criado anteriormente:

body {
    font-family: 'Open Sans', sans-serif;
    background: #fdfdfd;
}

#blockA, #blockB, #equal {
    float: left;
    display: block;
}

#blockA, #blockB {
    width: 44%;
}

h2 {
    text-align: center;
    color: #565656;
}

#container {
    margin: 0 auto;
    display: block;
    width: 75%;
}

input, select {
    width: 100%;
    float: left;
    margin-bottom: 15px;
    cursor: pointer;
    padding: 10px;
    border: 1px solid #ddd;
    -ms-box-sizing: border-box;
    -moz-box-sizing: border-box;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    background:#fff;
}

select {
    background: #f5f5f5;
}

#equal {
    margin: 10px 2px;
    font-weight: bold;
    width: 10%;
    text-align: center;
    font-size: 40px;
    color: #444;
}

Agora execute o projeto com npm start. Aqui está nossa tela.

Agora sim, vamos aos testes!

O que faremos é testar os métodos de conversão do nosso componente. Porém antes precisamos instalar as dependências.

O Jest por padrão já vem configurado quando o projeto é criado com Create React App, então só precisamos instalar o Enzyme através do comando: npm i –save-dev enzyme enzyme-adapter-react-16

Nota: É necessário instalar o Enzyme junto com um adaptador correspondente à versão do React (que neste caso é a 16).

Após isso, no começo do arquivo TempConverter.test.js inclua:

import React from 'react';
import Adapter from 'enzyme-adapter-react-16';
import { shallow, configure } from 'enzyme';
import TempConverter from './TempConverter';

configure({adapter: new Adapter()});

Adicionamos os imports e configuramos o adaptador que desejamos utilizar. Agora, logo abaixo, podemos dar início aos grupos de teste, a partir dos métodos da temperatura Celsius:

// Grupo de testes Celsius
describe('Verifica se os métodos de conversão para temperatura Celsius funcionam corretamente', () => {

    const wrapper = shallow(<TempConverter />);

    test('20°C igual é igual a 68°F', () => {
        expect(wrapper.instance().celsiusToFahrenheit(20) === 68.00).toBeTruthy();
    });
    test('32°C não é maior que 400°K', () => {
        expect(wrapper.instance().celsiusToKelvin(32) > 400).toBeFalsy();
    });
    test('59,3°C é igual a 138,75°F', () => {
        expect(wrapper.instance().celsiusToFahrenheit(59.3)).toEqual(138.75);
    });
})

Começamos sempre descrevendo do que se trata o grupo de testes, depois criamos uma instância do componente que desejamos chamar os métodos e por fim utilizamos a função test para avaliar os valores retornados pelos matchers do Jest, abaixo um resumo com alguns:

  • toBeNull corresponde apenas a null;
  • toBeUndefined corresponde apenas a valores indefinidos;
  • toBeDefined é o oposto de toBeUndefined;
  • toBeTruthy corresponde a qualquer coisa que uma declaração if trata como verdadeira;
  • toBeFalsy corresponde a qualquer coisa que uma instrução if trata como falsa;
  • toEqual compara recursivamente todas as propriedades da instância de um objeto.

Para conferir o resultado deste grupo de teste (preferencialmente com ajuda de uma calculadora J) digite no terminal:

npm test TempConverter.test.js

Veja que apenas um caso de teste falhou pois o valor esperado era 138.75, mas o método retornou 138.74.

Os testes são simples de realizar e o resultado é rápido. Podemos brincar mais um pouco e adicionar outros grupos de teste para avaliar os demais métodos:

// Grupo de testes Fahrenheit
describe('Verifica se os métodos de conversão para temperatura Fahrenheit funcionam corretamente', () => {
 
    const wrapper = shallow(<TempConverter />);

    test('28,988°F não é igual a 271,49°F', () => {
        expect(wrapper.instance().fahrenheitToCelsius(28.988)).not.toEqual(271.49);
    });
    test('A conversão de "30,22"°F para °C retorna um valor não numérico', () => {
        expect(wrapper.instance().fahrenheitToCelsius('30,22')).toBeNaN();
    });
    test('20°F é menor que 300°K', () => {
        expect(wrapper.instance().fahrenheitToKelvin(20)).toBeLessThan(300);
    });

})

// Grupo de testes Kelvin
describe('Verifica se os métodos de conversão para temperatura Kelvin funcionam corretamente', () => {

    const wrapper = shallow(<TempConverter />);

    test('800,3°K é próximo de 980,90°F', () => {
        expect(wrapper.instance().kelvinToFahrenheit(800.3)).toBeCloseTo(980.90, 1);
    });
})

E teremos um novo resultado:

Esse foi mais um artigo sobre a biblioteca React, não sou um especialista no assunto, diga-se de passagem, mas espero que tenha sido útil para você que está começando, qualquer dúvida ou correção não deixe de comentar, estamos sempre abertos à discussão!

Compartilhe!