Creación de aplicaciones React "The Agile Way"

React se ha convertido en los últimos tiempos en una de las librerías más utilizadas a la hora de afrontar el problema de la comunicación con el DOM. Gracias al trabajo de muchos desarrolladores y divulgadores, existen multitud de recursos tanto gratuitos como de pago que hacen que aprender React sea una tarea relativamente sencilla, así que no nos vamos a centrar tanto en el manejo de propiedades o en la gestión del estado en React, sino más en su uso como solución general dentro de un contexto Agile.

Este artículo no va de porqué React y no Angular 4 o VueJS. Si esperabas un flame al respecto siento decepcionarte. Este artículo se centra en como hacer que una herramienta como React se alinee con nuestra forma de enfocar el desarrollo de software.

La iteración 0

Cada vez que iniciamos un nuevo proyecto debemos crear la estructura del mismo y preparar las herramientas necesarias para ir añadiendo funcionalidad de forma progresiva haciéndola llegar a producción de la forma más rápida y efectiva. Esta construcción incial en la "iteración 0", se conoce como Walking Skeleton y está descrita en detalle en el Growing Object-Oriented Software, Guided by Tests, siendo además de vital importancia para garantizar una correcta evolución del proyecto. Si no estáis familiarizados con el concepto, la idea principal es intentar que la primera historia de usuario que implementemos en el sistema, por muy mínima que sea, actue como una bala trazadora obligándonos a definir todo nuestro sistema, construcción, despliegue y puesta en producción desde el principio. De esta forma, cada vez que terminemos una historia de usuario, esta podrá acabar en producción de una forma sencilla y predecible al haber implementado desde el principio todo el ciclo completo de construcción y despliegue.

En el caso de React y comenzando por la creación del proyecto, podemos generar esta estructura inicial mediante una utilidad que se llama create-react-app. Con create-react-app podemos gestionar un proyecto React sin tener que lidiar de forma directa con las complejidades del tooling de nueva generación que envuelve a la mayoría de proyectos JavaScripts actuales. Un pequeño infierno que debes aprender a controlar dominando Babel, NPM, Webpack y algunos otros amigos con los que mantener una relación de amor/odio.

Con create-react-app tenemos gratis:

  • Babel: React, JSX y ES6.
  • Chequeo de tipos con Flow.
  • Linting con ESLint.
  • Webpack: Dev server + soporte para los loaders de CSS e imágenes.
  • Testing con Jest.
  • Autoprefixer.
  • Build script para generación de versiones de producción, con imágenes optimizadas y soporte para Source Maps.
  • Offline-first service worker y webapp manifest para Progressive Web Apps.

Si no te gusta tanta magia negra, siempre puedes ejecutar el comando npm run eject y hacer explícita la configuración utilizada (toda la config se guarda en la raíz de tu proyecto), pudiendo así adaptar tu aplicación a cualquier configuración no soportada inicialmente por create-react-app.

Por supuesto, a la parte cliente tenemos que añadir las llamadas a nuestro backend que expondrá la funcionalidad deseada como servicio. En cualquier caso, simplemente recordaros que para poder conectar con la parte del servidor sin tener problemas de CORS, podemos utilizar la opción de proxy de nuestro servidor de desarrollo.

Empezando la casa por las historias de usuario

Cualquier funcionalidad en un proyecto Agile nace gracias a su formalización como historia de usuario. Aunque todo esto lo explica Mike Cohn muchísimo mejor que yo, me gustaría centrarme en un aspecto que para mi es clave en su escritura y que cada vez veo a más equipos obviar a la hora de crear una buena historia de usuario: El Definition of Done o DoD para los amigos.

Sin el DoD, no tenemos una forma clara de verificar que las expectativas de la historia de usuario se han satisfecho una vez finalizada. Es la descripción formal, sistematizada y reproducible del incremento de valor aportado y, como tal, tiene que ayudarnos a guiar nuestro desarrollo dirigido por pruebas.

"No se que tengo que testear" es una de la afirmaciones que más sueles escuchar cuando alguien está comenzando a introducirse a XP (ojo que XP no es un sistema operativo obsoleto que aun vaga por el mundo, sino un conjunto de prácticas técnicas sintetizadas por Kent Bech en Extreme Programming Explained).

¿Como comenzar entonces mis primeras pruebas? Pues si seguimos una aproximación de fuera hacia dentro (outside-in TDD), el primer test del que partiremos será un test de aceptación construido a partir de la definición del DoD que nuestro Product Owner ha escrito para nosotros. Si todo esto te suena un poco a chino y quieres saber más acerca de los tests de aceptación o de testing en general, puedes echarle un ojo a Test-Driven Development by Example de Kent Beck o Growing Object-Oriented Software, Guided by Tests de Steve Freeman.

Centrando un poco más el tema, nuestro objetivo es que cada funcionalidad que se defina en mi backlog en forma de historia de usuario tenga su DoD y pueda dar lugar a un test de aceptación que guíe el desarrollo. Para este primer test de aceptación, nosotros no usamos el concepto de historia de usuario "ejecutable" que podemos ver en Cucumber en su definición más BDD y/o ATDD, sino que es el equipo técnico el que escribe estos tests a partir del DoD y los automatiza para que se ejecuten a través del navegador (aunque comenzamos hace un tiempo con PhantomJS, ahora estamos pasando a Chrome headless y desde hace nada siguiendo muy de cerca el trabajo detrás de Chrome Puppeteer).

Si ya conoces bien la comunidad JavaScript, sabrás que existen mil millones de frameworks para cada cosa, y el tema del testing no es una excepción. Desde la base definida por Selenium y WebDriver podemos encontrar una amplísima gama de opciones con la que tendremos que lidiar antes de poder poner a funcionar nuestro primer test de aceptación, así que si las opciones elegidas para este ejemplo no te convencen, no te preocupes, hay mucho donde elegir!!!

Como curiosidad, comentar que nosotros usamos NightWatch para lanzar este tipo de pruebas y que, para evitar la fragilidad de los tests por cambios en el interfaz, los puntos del UI son accionados mediante una abstracción que definimos sobre los componentes siguiendo el patrón Page Object.

Siguiendo nuestro enfoque dirigido por las pruebas de aceptación, deberíamos de tener el test fallando y en rojo y el Page Object correspondiente definido pero sin posibilidad de interaccionar con ningún componente del interfaz. Estamos en el buen camino!! Sigamos pues adelante :)

Tests de integración de componentes

Con un test de aceptación en rojo que mantendremos durante el tiempo que tardemos en satisfacer el DoD de la historia de usuario, es hora de comenzar a implementar la funcionalidad deseada mediante la interacción de distintos componentes dentro del interfaz.

Es en este momento momento cuando herramientas como enzyme, gracias al shallow rendering que permite React, nos permitirán ir definiendo como se deben comportar en cuanto a interacción y en cuanto a definición nuestros componentes.

Antes de nada, añadimos las dependencias necesarias a nuestro proyecto, teniendo en cuenta que, si hemos usado create-react-app tal y como hemos comentado anteriormente, Jest será el framework de test que tendremos como base para la ejecución de los tests.


npm install --save-dev chai enzyme react-test-renderer

Una vez todo instalado, ya podemos crear un test en nuestro proyecto.

Aunque se puede hacer de muchas formas, Jest soporta la definición de directorios __tests__ en los distintos niveles de nuestra estructura de código, de forma que podemos dejar en estos directorios las clases de test y los recursos que utilicen en ellas.

Como ejemplo, podemos ver este test que prueba un componente que lista películas de StarWars haciendo uso de un Page Object:


jest.mock('../Repository');

import React from 'react';
import { mount } from 'enzyme';

import App from '../../App';
import Films from '../Films';

const FILM_TITLES = [
  'The Phantom Menace', 
  'Attack of the Clones', 
  'Revenge of the Sith', 
  'A New Hope', 
  'The Empire Strikes Back', 
  'Return of the Jedi', 
  'The Force Awakens'
];

const FILM_EPISODES = FILM_TITLES.map((title, index) => index+1);
const NUMBER_OF_FILMS = FILM_TITLES.length;

describe('Films', () => {
  let wrapper;
  let films;

  beforeEach(() => {
    wrapper = mount();
    films = new Films(wrapper);
  });

  it('should be listed', async () => {
    expect(films.obtainFilms()).toHaveLength(NUMBER_OF_FILMS);   
  });

  it('should show name and episode number', async () => {
    const filmsTitles = films.obtainFilmsTitles();
    expect(filmsTitles).toEqual(FILM_TITLES);

    const filmsEpisodes = films.obtainFilmsEpisodes();
    expect(filmsEpisodes).toEqual(FILM_EPISODES);
  });

  it('should be ordered by episode number', async () => {
    films.obtainFilmsEpisodes().forEach((episode, index) => {
      expect(episode).toEqual(index + 1);
    });
  });
});

Como podemos ver en la primera linea de código del ejemplo, se hace uso de un doble de test o mock, de forma que los datos no se cargan realmente del origen HTTP, sino que se definen para servir ciertas fixtures dentro de un directorio especial que en Jest se llama "\_\_mocks\_\_".

Así pues, sólo nos quedaría echar un vistazo al Page Object utilizado para el acceso a la estructura de películas:


export default class Films {
  constructor(wrapper) {
    this.wrapper = wrapper;
  }
 
  obtainFilms() {
    return this.wrapper.find('.film');
  }
  
  obtainFilmsTitles() {
    return this.wrapper.find('.film .title').map((film) => film.text());
  }

  obtainFilmsEpisodes() {
    return this.wrapper.find('.film .episode').map((film) => parseInt(film.text(), 10));
  }
}

Y, como no, aquí tenemos el componente principal:


import React, { Component } from 'react';

import repository from './Repository';

export default class FilmsPanel extends Component {
  constructor(props) {
    super(props);

    this.state = {
      films: []
    }
  }

  componentWillMount() {
    repository.retrieveFilms()
      .then((response) => {
        this.setState({
          films: response.results
        });
      });
  }

  render() {
    return (
      <div>
        {
          this.state.films.map((film) => {
            return (
              <div className="film" key={film.episode_id}>
                <div className="title">{film.title}</div>
                <div className="episode">{film.episode_id}</div>
              </div>
            );
          })
        }
      </div>
    );
  }
}

Una vez añadido todo el código de test necesario a nuestro proyecto, tendremos que lanzar el comando npm test para ver como la ejecución de las pruebas se queda en modo "watch", esperando cambios por nuestra parte:

Con este ciclo que hemos abierto, sólo debemos continuar iterando en estas definiciones hasta que todos los requisitos del DoD se hayan satisfecho y el test de aceptación general pase por fin de rojo a verde. Well done!!! :)

Conclusión

El framework principal de elección es sólo una parte ínfima del conjunto de decisiones que debemos llegar a tomar para diseñar un proceso de construcción escalable y de calidad. Prácticas como el testing, la integración continua y la correcta definición de historias de usuario tienen un peso intrínseco infinitamente mayor que ser fan y/o gurú de React o de VueJS. Comencemos hoy a interiorizar prácticas independientes de la tecnología empleada y nos convertiremos con el tiempo en verdaderos Agile Developers.

¿Qué practicas estás incorporando a tu ciclo de desarrollo con el fin de mejorarlo día a día?

Ver todos los comentarios en https://www.genbeta.com

VER 0 Comentario

Portada de Genbeta