Desenvolvimento Orientado a Testes React: Das Histórias do Usuário à Produção

Publicados: 2022-03-11

Neste post, vamos desenvolver um aplicativo React usando desenvolvimento orientado a testes (TDD) de histórias de usuários para desenvolvimento. Além disso, vamos usar Jest e Enzyme para o TDD. Ao concluir este guia, você será capaz de:

  • Crie épicos e histórias de usuários com base nos requisitos.
  • Crie testes com base em histórias de usuários.
  • Desenvolva um aplicativo React usando TDD.
  • Use Enzyme e Jest para testar um aplicativo React.
  • Use/reutilize variáveis ​​CSS para design responsivo.
  • Crie um componente React reutilizável que renderiza e funciona de maneira diferente com base nas props fornecidas.
  • Digite adereços do componente de verificação usando React PropTypes.

Este artigo pressupõe que você tenha conhecimento básico de React. Se você é completamente novo no React, eu recomendo que você complete o tutorial oficial e dê uma olhada no Tutorial React 2019 da Toptal: Parte 1 e Parte 2.

Visão geral do nosso aplicativo React orientado a testes

Construiremos um aplicativo de timer pomodoro básico que consiste em alguns componentes de interface do usuário. Cada componente terá um conjunto separado de testes em um arquivo de teste correspondente. Em primeiro lugar, poderíamos criar épicos e histórias de usuários da seguinte forma, com base em nossos requisitos de projeto.

ÉPICO HISTÓRIA DO USUÁRIO CRITÉRIOS DE ACEITAÇÃO
Como usuário, preciso usar o cronômetro para poder gerenciar meu tempo. Como usuário, preciso iniciar o cronômetro para poder contar meu tempo. Certifique-se de que o usuário seja capaz de:

*inicia o cronômetro
*veja o cronômetro começar a contagem regressiva

A contagem regressiva do tempo não deve ser interrompida mesmo se o usuário clicar no botão iniciar mais de uma vez.
Como usuário, preciso parar o cronômetro para poder contar meu tempo apenas quando necessário. Certifique-se de que o usuário seja capaz de:

*parar o cronômetro
*veja o cronômetro parado

Nada deve acontecer mesmo se o usuário clicar no botão Parar mais de uma vez.
Como usuário, preciso redefinir o cronômetro para poder contar meu tempo desde o início. Certifique-se de que o usuário seja capaz de:

* redefinir o temporizador
*veja o temporizador redefinido para o padrão

Estrutura de arame

Estrutura de arame

Configuração do projeto

Primeiro, vamos criar um projeto React usando Create React App da seguinte forma:

 $ npx create-react-app react-timer $ cd react-timer $ npm start

Você verá uma nova guia do navegador aberta na URL http://localhost:3000. Você pode parar o aplicativo React em execução usando Ctrl+C .

Agora, vamos adicionar Jest e Enzyme e algumas dependências da seguinte forma:

 $ npm i -D enzyme $ npm i -D react-test-renderer enzyme-adapter-react-16

Além disso, adicionaremos ou atualizaremos um arquivo chamado setupTests.js no diretório src :

 import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; configure({ adapter: new Adapter() });

Como o Create React App executa o arquivo setupTests.js antes de cada teste, ele executará e configurará corretamente o Enzyme.

Configurando CSS

Vamos escrever variáveis ​​e uma redefinição básica de CSS porque queremos que as variáveis ​​CSS estejam globalmente disponíveis no aplicativo. Vamos definir as variáveis ​​do escopo :root. A sintaxe para definir variáveis ​​é usar a notação de propriedade customizada, cada uma começando com – seguido pelo nome da variável.

Navegue até o arquivo index.css e adicione o seguinte:

 :root { --main-font: “Roboto”, sans-serif; } body, div, p { margin: 0; padding: 0; }

Agora, precisamos importar o CSS para nossa aplicação. Atualize o arquivo index.js da seguinte maneira:

 import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode> document.getElementById(“root”) )

Teste de renderização superficial

Como você já deve saber, o processo TDD ficaria assim:

  1. Adicione um teste.
  2. Execute todos os testes e você verá que o teste falha.
  3. Escreva o código para passar no teste.
  4. Execute todos os testes.
  5. Refatorar.
  6. Repetir.

Portanto, vamos adicionar o primeiro teste para um teste de renderização superficial e, em seguida, escrever o código para passar no teste. Adicione um novo arquivo de especificação chamado App.spec.js ao diretório src/components/App da seguinte forma:

 import React from 'react'; import { shallow } from 'enzyme'; import App from './App'; describe('App', () => { it('should render a <div />', () => { const container = shallow(<App />); expect(container.find('div').length).toEqual(1); }); });

Então, você pode executar o teste:

 $ npm test

Você verá que o teste falha.

Componente do aplicativo

Agora, continuaremos a criar o componente App para passar no teste. Navegue até App.jsx no diretório src/components/App e adicione o código da seguinte forma:

 import React from 'react'; const App = () => <div className=”app-container” />; export default App;

Agora, execute o teste novamente.

 $ npm test

O primeiro teste deve passar agora.

Adicionando CSS do aplicativo

Vamos criar um arquivo App.css no diretório src/components/App para adicionar algum estilo ao componente App da seguinte forma:

 .app-container { height: 100vh; width: 100vw; align-items: center; display: flex; justify-content: center; }

Agora, estamos prontos para importar o CSS para o arquivo App.jsx :

 import React from 'react'; import './App.css'; const App = () => <div className=”app-container” />; export default App;

Em seguida, temos que atualizar o arquivo index.js para importar o componente App da seguinte forma:

 import React from "react" import ReactDOM from "react-dom" import "./index.css" import App from "./components/App/App" import * as serviceWorker from "./serviceWorker" ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") ) // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister()

Adicionando o componente Timer

Por fim, o aplicativo conterá o componente Timer, portanto, atualizaremos o arquivo App.spec.js para verificar a presença do componente Timer em nosso aplicativo. Além disso, vamos declarar a variável container fora do primeiro caso de teste, pois o teste de renderização superficial precisa ser feito antes de cada caso de teste.

 import React from "react" import { shallow } from "enzyme" import App from "./App" import Timer from "../Timer/Timer" describe("App", () => { let container beforeEach(() => (container = shallow(<App />))) it("should render a <div />", () => { expect(container.find("div").length).toEqual(1) }) it("should render the Timer Component", () => { expect(container.containsMatchingElement(<Timer />)).toEqual(true) }) })

Se você executar npm test neste estágio, o teste falhará, pois o componente Timer ainda não existe.

Escrevendo o teste de renderização superficial do temporizador

Agora, vamos criar um arquivo chamado Timer.spec.js em um novo diretório chamado Timer no diretório src/components .

Além disso, adicionaremos o teste de renderização superficial no arquivo Timer.spec.js :

 import React from "react" import { shallow } from "enzyme" import Timer from "./Timer" describe("Timer", () => { let container beforeEach(() => (container = shallow(<Timer />))) it("should render a <div />", () => { expect(container.find("div").length).toBeGreaterThanOrEqual(1) }) })

O teste falhará, como esperado.

Criando o componente Timer

Em seguida, vamos criar um novo arquivo chamado Timer.jsx e definir as mesmas variáveis ​​e métodos com base nas histórias do usuário:

 import React, { Component } from 'react'; class Timer extends Component { constructor(props) { super(props); this.state = { minutes: 25, seconds: 0, isOn: false }; } startTimer() { console.log('Starting timer.'); } stopTimer() { console.log('Stopping timer.'); } resetTimer() { console.log('Resetting timer.'); } render = () => { return <div className="timer-container" />; }; } export default Timer;

Isso deve passar no teste e renderizar um <div /> no arquivo Timer.spec.js , mas o teste não deve renderizar o componente Timer, pois ainda não adicionamos o componente Timer no componente app.

Vamos adicionar o componente Timer no arquivo App.jsx assim:

 import React from 'react'; import './App.css'; import Timer from '../Timer/Timer'; const App = () => ( <div className="app-container"> <Timer /> </div> ); export default App;

Todos os testes devem passar agora.

Adicionando CSS do temporizador

Vamos adicionar variáveis ​​CSS relacionadas ao Timer e adicionar consultas de mídia para dispositivos menores.

Atualize o arquivo index.css da seguinte forma:

 :root { --timer-background-color: #FFFFFF; --timer-border: 1px solid #000000; --timer-height: 70%; --timer-width: 70%; } body, div, p { margin: 0; padding: 0; } @media screen and (max-width: 1024px) { :root { --timer-height: 100%; --timer-width: 100%; } }

Além disso, vamos criar o arquivo Timer.css no diretório components/Timer :

 .timer-container { background-color: var(--timer-background-color); border: var(--timer-border); height: var(--timer-height); width: var(--timer-width); }

Temos que atualizar o Timer.jsx para importar o arquivo Timer.css .

 import React, { Component } from "react" import "./Timer.css"

Se você executar o aplicativo React agora, verá uma tela simples com a borda no seu navegador.

Escreva o teste de renderização superficial do TimerButton

Precisamos de três botões: Start, Stop e Reset , por isso vamos criar o TimerButton Component .

Primeiro, precisamos atualizar o arquivo Timer.spec.js para verificar a existência do componente TimerButton no componente Timer :

 it("should render instances of the TimerButton component", () => { expect(container.find("TimerButton").length).toEqual(3) })

Agora, vamos adicionar o arquivo TimerButton.spec.js em um novo diretório chamado TimerButton no diretório src/components e vamos adicionar o teste ao arquivo assim:

 import React from "react" import { shallow } from "enzyme" import TimerButton from "./TimerButton" describe("TimerButton", () => { let container beforeEach(() => { container = shallow( <TimerButton buttonAction={jest.fn()} buttonValue={""} /> ) }) it("should render a <div />", () => { expect(container.find("div").length).toBeGreaterThanOrEqual(1) }) })

Agora, se você executar o teste, verá que o teste falha.

Vamos criar o arquivo TimerButton.jsx para o componente TimerButton :

 import React from 'react'; import PropTypes from 'prop-types'; const TimerButton = ({ buttonAction, buttonValue }) => ( <div className="button-container" /> ); TimerButton.propTypes = { buttonAction: PropTypes.func.isRequired, buttonValue: PropTypes.string.isRequired, }; export default TimerButton;

Se você executar npm test neste estágio, o teste deverá renderizar instâncias do componente TimerButton, mas falhará, pois ainda não adicionamos os componentes TimerButton ao componente Timer.

Vamos importar o componente TimerButton e adicionar três componentes TimerButton no método render em Timer.jsx :

 render = () => { return ( <div className="timer-container"> <div className="time-display"></div> <div className="timer-button-container"> <TimerButton buttonAction={this.startTimer} buttonValue={'Start'} /> <TimerButton buttonAction={this.stopTimer} buttonValue={'Stop'} /> <TimerButton buttonAction={this.resetTimer} buttonValue={'Reset'} /> </div> </div> ); };

CSS do botão do temporizador

Agora, é hora de adicionar variáveis ​​CSS para o componente TimerButton. Vamos adicionar variáveis ​​no escopo :root ao arquivo index.css :

 :root { ... --button-border: 3px solid #000000; --button-text-size: 2em; } @media screen and (max-width: 1024px) { :root { … --button-text-size: 4em; } }

Além disso, vamos criar um arquivo chamado TimerButton.css no diretório TimerButton no diretório src/components :

 .button-container { flex: 1 1 auto; text-align: center; margin: 0px 20px; border: var(--button-border); font-size: var(--button-text-size); } .button-container:hover { cursor: pointer; }

Vamos atualizar o TimerButton.jsx de acordo para importar o arquivo TimerButton.css e exibir o valor do botão:

 import React from 'react'; import PropTypes from 'prop-types'; import './TimerButton.css'; const TimerButton = ({ buttonAction, buttonValue }) => ( <div className="button-container"> <p className="button-value">{buttonValue}</p> </div> ); TimerButton.propTypes = { buttonAction: PropTypes.func.isRequired, buttonValue: PropTypes.string.isRequired, }; export default TimerButton;

Além disso, precisamos atualizar o Timer.css para alinhar os três botões horizontalmente, então vamos atualizar o arquivo Timer.css também:

 import React from 'react'; import PropTypes from 'prop-types'; import './TimerButton.css'; const TimerButton = ({ buttonAction, buttonValue }) => ( <div className="button-container"> <p className="button-value">{buttonValue}</p> </div> ); TimerButton.propTypes = { buttonAction: PropTypes.func.isRequired, buttonValue: PropTypes.string.isRequired, }; export default TimerButton;

Se você executar o aplicativo React agora, verá uma tela da seguinte forma:

Cronômetro

Refatorando o temporizador

Vamos refatorar o Timer já que queremos implementar funções como startTimer, stopTimer, restartTimer e resetTimer . Vamos atualizar o arquivo Timer.spec.js primeiro:

 describe('mounted Timer', () => { let container; beforeEach(() => (container = mount(<Timer />))); it('invokes startTimer when the start button is clicked', () => { const spy = jest.spyOn(container.instance(), 'startTimer'); container.instance().forceUpdate(); expect(spy).toHaveBeenCalledTimes(0); container.find('.start-timer').first().simulate('click'); expect(spy).toHaveBeenCalledTimes(1); }); it('invokes stopTimer when the stop button is clicked', () => { const spy = jest.spyOn(container.instance(), 'stopTimer'); container.instance().forceUpdate(); expect(spy).toHaveBeenCalledTimes(0); container.find('.stop-timer').first().simulate('click'); expect(spy).toHaveBeenCalledTimes(1); }); it('invokes resetTimer when the reset button is clicked', () => { const spy = jest.spyOn(container.instance(), 'resetTimer'); container.instance().forceUpdate(); expect(spy).toHaveBeenCalledTimes(0); container.find('.reset-timer').first().simulate('click'); expect(spy).toHaveBeenCalledTimes(1); }); });

Se você executar o teste, verá os testes adicionados falharem, pois ainda não atualizamos o componente TimerButton . Vamos atualizar o componente TimerButton para adicionar o evento click:

 const TimerButton = ({ buttonAction, buttonValue }) => ( <div className="button-container" onClick={() => buttonAction()}> <p className="button-value">{buttonValue}</p> </div> );

Agora, os testes devem passar.

Em seguida, vamos adicionar mais testes para verificar o estado quando cada função é invocada no caso de teste Timer montado :

 it('should change isOn state true when the start button is clicked', () => { container.instance().forceUpdate(); container.find('.start-timer').first().simulate('click'); expect(container.instance().state.isOn).toEqual(true); }); it('should change isOn state false when the stop button is clicked', () => { container.instance().forceUpdate(); container.find('.stop-timer').first().simulate('click'); expect(container.instance().state.isOn).toEqual(false); }); it('should change isOn state false when the reset button is clicked', () => { container.instance().forceUpdate(); container.find('.stop-timer').first().simulate('click'); expect(container.instance().state.isOn).toEqual(false); expect(container.instance().state.minutes).toEqual(25); expect(container.instance().state.seconds).toEqual(0); });

Se você executar os testes, verá que eles falham, pois ainda não implementamos cada método. Então vamos implementar cada função para passar nos testes:

 startTimer() { this.setState({ isOn: true }); } stopTimer() { this.setState({ isOn: false }); } resetTimer() { this.stopTimer(); this.setState({ minutes: 25, seconds: 0, }); }

Você verá os testes passarem se os executar. Agora, vamos implementar as funções restantes em Timer.jsx :

 import React, { Component } from 'react'; import './Timer.css'; import TimerButton from '../TimerButton/TimerButton'; class Timer extends Component { constructor(props) { super(props); this.state = { minutes: 25, seconds: 0, isOn: false, }; this.startTimer = this.startTimer.bind(this); this.stopTimer = this.stopTimer.bind(this); this.resetTimer = this.resetTimer.bind(this); } startTimer() { if (this.state.isOn === true) { return; } this.myInterval = setInterval(() => { const { seconds, minutes } = this.state; if (seconds > 0) { this.setState(({ seconds }) => ({ seconds: seconds - 1, })); } if (seconds === 0) { if (minutes === 0) { clearInterval(this.myInterval); } else { this.setState(({ minutes }) => ({ minutes: minutes - 1, seconds: 59, })); } } }, 1000); this.setState({ isOn: true }); } stopTimer() { clearInterval(this.myInterval); this.setState({ isOn: false }); } resetTimer() { this.stopTimer(); this.setState({ minutes: 25, seconds: 0, }); } render = () => { const { minutes, seconds } = this.state; return ( <div className="timer-container"> <div className="time-display"> {minutes}:{seconds < 10 ? `0${seconds}` : seconds} </div> <div className="timer-button-container"> <TimerButton className="start-timer" buttonAction={this.startTimer} buttonValue={'Start'} /> <TimerButton className="stop-timer" buttonAction={this.stopTimer} buttonValue={'Stop'} /> <TimerButton className="reset-timer" buttonAction={this.resetTimer} buttonValue={'Reset'} /> </div> </div> ); }; } export default Timer;

Você verá todas as funções funcionando com base nas histórias de usuários que preparamos anteriormente.

Cronômetro

Então, foi assim que desenvolvemos um aplicativo React básico usando TDD. Se as histórias de usuários e os critérios de aceitação forem mais detalhados, os casos de teste poderão ser escritos com mais precisão, contribuindo ainda mais.

Empacotando

Ao desenvolver um aplicativo usando TDD, é muito importante não apenas dividir o projeto em épicos ou histórias de usuários, mas também preparar-se bem para os critérios de aceitação. Neste artigo, eu queria mostrar como dividir o projeto e usar os critérios de aceitação preparados para o desenvolvimento do React TDD.

Embora existam muitos recursos relacionados ao React TDD por aí, espero que este artigo tenha ajudado você a aprender um pouco sobre o desenvolvimento de TDD com React usando histórias de usuários. Se você optar por emular essa abordagem, consulte o código-fonte completo aqui.