Desarrollo basado en pruebas de React: de las historias de usuario a la producción

Publicado: 2022-03-11

En esta publicación, vamos a desarrollar una aplicación React utilizando el desarrollo basado en pruebas (TDD) desde las historias de los usuarios hasta el desarrollo. Además, vamos a utilizar Jest y Enzyme para el TDD. Al completar esta guía, podrá:

  • Cree epopeyas e historias de usuarios en función de los requisitos.
  • Cree pruebas basadas en historias de usuarios.
  • Desarrolle una aplicación React usando TDD.
  • Use Enzyme y Jest para probar una aplicación React.
  • Use/reutilice variables CSS para un diseño receptivo.
  • Cree un componente React reutilizable que represente y funcione de manera diferente en función de los accesorios proporcionados.
  • Escriba accesorios de componente de verificación usando React PropTypes.

Este artículo asume que tienes conocimientos básicos de React. Si es completamente nuevo en React, le recomiendo que complete el tutorial oficial y eche un vistazo al Tutorial de React 2019 de Toptal: Parte 1 y Parte 2.

Descripción general de nuestra aplicación React basada en pruebas

Construiremos una aplicación de temporizador pomodoro básica que constará de algunos componentes de interfaz de usuario. Cada componente tendrá un conjunto separado de pruebas en un archivo de prueba correspondiente. En primer lugar, podríamos crear epopeyas e historias de usuarios de la siguiente manera según los requisitos de nuestro proyecto.

ÉPICO HISTORIA DEL USUARIO CRITERIOS DE ACEPTACIÓN
Como usuario, necesito usar el temporizador para poder administrar mi tiempo. Como usuario, necesito iniciar el temporizador para poder contar mi tiempo. Asegúrese de que el usuario pueda:

* iniciar el temporizador
* ver el temporizador empezar a contar hacia atrás

La cuenta regresiva del tiempo no debe interrumpirse incluso si el usuario hace clic en el botón de inicio más de una vez.
Como usuario, necesito detener el temporizador para poder contar mi tiempo solo cuando sea necesario. Asegúrese de que el usuario pueda:

* detener el temporizador
*ver el temporizador detenido

No debería pasar nada incluso si el usuario hace clic en el botón de parada más de una vez.
Como usuario, necesito restablecer el temporizador para poder contar mi tiempo desde el principio. Asegúrese de que el usuario pueda:

*reiniciar el temporizador
* ver el temporizador restablecer al valor predeterminado

Estructura alámbrica

Estructura alámbrica

Configuración del proyecto

Primero, vamos a crear un proyecto React usando Create React App de la siguiente manera:

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

Verá una nueva pestaña del navegador abierta en la URL http://localhost:3000. Puede detener la ejecución de la aplicación React usando Ctrl+C .

Ahora, vamos a agregar Jest y Enzyme y algunas dependencias de la siguiente manera:

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

Además, agregaremos o actualizaremos un archivo llamado setupTests.js en el directorio src :

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

Dado que Create React App ejecuta el archivo setupTests.js antes de cada prueba, ejecutará y configurará correctamente Enzyme.

Configuración de CSS

Vamos a escribir variables y un reinicio básico de CSS porque queremos que las variables de CSS estén disponibles globalmente en la aplicación. Definiremos las variables desde el ámbito :root. La sintaxis para definir variables es usar una notación de propiedad personalizada, cada una de las cuales comienza con – seguida del nombre de la variable.

Navegue hasta el archivo index.css y agregue lo siguiente:

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

Ahora, necesitamos importar el CSS a nuestra aplicación. Actualice el archivo index.js de la siguiente manera:

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

Prueba de renderizado superficial

Como ya sabrá, el proceso de TDD se vería así:

  1. Añadir una prueba.
  2. Ejecute todas las pruebas y verá que la prueba falla.
  3. Escribe el código para pasar la prueba.
  4. Ejecute todas las pruebas.
  5. Refactorizar.
  6. Repetir.

Por lo tanto, agregaremos la primera prueba para una prueba de renderización superficial y luego escribiremos el código para pasar la prueba. Agregue un nuevo archivo de especificaciones llamado App.spec.js al directorio src/components/App de la siguiente manera:

 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); }); });

Luego, puedes ejecutar la prueba:

 $ npm test

Verás que la prueba falla.

Componente de la aplicación

Ahora, procederemos a crear el componente App para pasar la prueba. Navegue a App.jsx en el directorio src/components/App y agregue el código de la siguiente manera:

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

Ahora, vuelve a ejecutar la prueba.

 $ npm test

La primera prueba debería pasar ahora.

Agregar aplicación CSS

Vamos a crear un archivo App.css en el directorio src/components/App para agregar algo de estilo al componente de la aplicación de la siguiente manera:

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

Ahora, estamos listos para importar el CSS al archivo App.jsx :

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

A continuación, tenemos que actualizar el archivo index.js para importar el componente de la aplicación de la siguiente manera:

 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()

Agregar el componente de temporizador

Finalmente, la aplicación contendrá el componente Timer, por lo tanto, actualizaremos el archivo App.spec.js para verificar la presencia del componente Timer en nuestra aplicación. Además, vamos a declarar la variable contenedora fuera del primer caso de prueba, ya que la prueba de representación superficial debe realizarse antes de cada caso de prueba.

 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) }) })

Si ejecuta npm test en esta etapa, la prueba fallará ya que el componente del temporizador aún no existe.

Escribir la prueba de representación superficial del temporizador

Ahora, vamos a crear un archivo llamado Timer.spec.js en un nuevo directorio llamado Timer en el directorio src/components .

Además, agregaremos la prueba de procesamiento superficial en el archivo 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) }) })

La prueba fallará, como se esperaba.

Creación del componente de temporizador

A continuación, creemos un nuevo archivo llamado Timer.jsx y definamos las mismas variables y métodos basados ​​en las historias de los usuarios:

 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;

Esto debería pasar la prueba y debería generar un <div /> en el archivo Timer.spec.js , pero la prueba no debería generar el componente de temporizador ya que aún no hemos agregado el componente de temporizador en el componente de la aplicación.

Vamos a agregar el componente Timer en el archivo App.jsx de esta manera:

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

Todas las pruebas deberían pasar ahora.

Agregar temporizador CSS

Agregaremos variables CSS relacionadas con el temporizador y agregaremos consultas de medios para dispositivos más pequeños.

Actualice el archivo index.css de la siguiente manera:

 :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%; } }

Además, vamos a crear el archivo Timer.css en el directorio components/Timer :

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

Tenemos que actualizar Timer.jsx para importar el archivo Timer.css .

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

Si ejecuta la aplicación React ahora, verá una pantalla simple con el borde en su navegador.

Escriba la prueba de representación superficial de TimerButton

Necesitamos tres botones: Start, Stop y Reset , por lo que vamos a crear el componente TimerButton .

Primero, necesitamos actualizar el archivo Timer.spec.js para verificar la existencia del componente TimerButton en el componente Timer :

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

Ahora, agreguemos el archivo TimerButton.spec.js en un nuevo directorio llamado TimerButton en el directorio src/components y agreguemos la prueba al archivo de esta manera:

 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) }) })

Ahora, si ejecuta la prueba, verá que la prueba falla.

Vamos a crear el archivo TimerButton.jsx para el 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;

Si ejecuta npm test en esta etapa, la prueba debería generar instancias del componente TimerButton pero fallará ya que aún no hemos agregado los componentes TimerButton al componente Timer.

Importemos el componente TimerButton y agreguemos tres componentes TimerButton en el método de representación en 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> ); };

Botón del temporizador CSS

Ahora es el momento de agregar variables CSS para el componente TimerButton. Agreguemos variables en el ámbito :root al archivo index.css :

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

Además, vamos a crear un archivo llamado TimerButton.css en el directorio TimerButton bajo el directorio 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; }

Actualicemos TimerButton.jsx en consecuencia para importar el archivo TimerButton.css y mostrar el valor del botón:

 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;

Además, necesitamos actualizar Timer.css para alinear los tres botones horizontalmente, así que actualicemos también el archivo Timer.css :

 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;

Si ejecuta la aplicación React ahora, verá una pantalla como la siguiente:

Temporizador

Refactorización del temporizador

Vamos a refactorizar el temporizador ya que queremos implementar funciones como startTimer, stopTimer, restartTimer y resetTimer . Primero actualicemos el archivo Timer.spec.js :

 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); }); });

Si ejecuta la prueba, verá que las pruebas agregadas fallan ya que aún no hemos actualizado el componente TimerButton . Actualicemos el componente TimerButton para agregar el evento de clic:

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

Ahora, las pruebas deberían pasar.

A continuación, agregaremos más pruebas para verificar el estado cuando se invoca cada función en el caso de prueba del temporizador 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); });

Si ejecuta las pruebas, verá que fallan ya que aún no hemos implementado cada método. Así que implementemos cada función para pasar las pruebas:

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

Verá que las pruebas pasan si las ejecuta. Ahora, implementemos las funciones restantes en 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;

Verá que todas las funciones funcionan según las historias de usuario que preparamos anteriormente.

Temporizador

Entonces, así es como hemos desarrollado una aplicación React básica usando TDD. Si las historias de usuario y los criterios de aceptación son más detallados, los casos de prueba se pueden escribir con mayor precisión, contribuyendo así aún más.

Terminando

Al desarrollar una aplicación con TDD, es muy importante no solo dividir el proyecto en épicas o historias de usuarios, sino también prepararse bien para los criterios de aceptación. En este artículo, quería mostrarle cómo desglosar el proyecto y utilizar los criterios de aceptación preparados para el desarrollo de React TDD.

Aunque existen muchos recursos relacionados con React TDD, espero que este artículo lo haya ayudado a aprender un poco sobre el desarrollo de TDD con React usando historias de usuarios. Si decide emular este enfoque, consulte el código fuente completo aquí.