Cómo los componentes de React facilitan las pruebas de interfaz de usuario

Publicado: 2022-03-11

Probar back-ends es fácil. Eliges el idioma de tu elección, lo combinas con tu marco favorito, escribes algunas pruebas y presionas "ejecutar". Su consola dice "¡Yay! ¡Funciona!" Su servicio de integración continua ejecuta sus pruebas en cada impulso, la vida es genial.

Claro, el desarrollo basado en pruebas (TDD) es extraño al principio, pero un entorno predecible, múltiples ejecutores de pruebas, herramientas de prueba integradas en marcos y soporte de integración continua hacen la vida más fácil. Hace cinco años pensé que las pruebas eran la solución a todos los problemas que he tenido.

Entonces Backbone se hizo grande.

Todos cambiamos a MVC front-end. Nuestros backends comprobables se convirtieron en servidores de bases de datos glorificados. Nuestro código más complicado se trasladó al navegador. Y nuestras aplicaciones ya no eran comprobables en la práctica.

Esto se debe a que probar el código front-end y los componentes de la interfaz de usuario es un poco difícil.

No está tan mal si lo único que queremos es comprobar que nuestros modelos se comportan bien. O bien, que llamar a una función cambiará el valor correcto. Todo lo que tenemos que hacer para las pruebas unitarias de React es:

  • Escriba módulos bien formados y aislados.
  • Use pruebas Jasmine o Mocha (o lo que sea) para ejecutar funciones.
  • Use un corredor de prueba, como Karma o Chutzpah.

Eso es todo. Nuestro código está probado por unidades.

Solía ​​ser que ejecutar pruebas de front-end era la parte difícil. Cada marco tenía sus propias ideas y, en la mayoría de los casos, terminaba con una ventana del navegador que actualizaba manualmente cada vez que deseaba ejecutar las pruebas. Por supuesto, siempre lo olvidarías. Al menos, sé que lo hice.

En 2012, Vojta Jina lanzó el corredor Karma (llamado Testacular en ese momento). Con Karma, las pruebas de front-end se convierten en un ciudadano completo de la cadena de herramientas. Nuestras pruebas de React se ejecutan en una terminal o en un servidor de integración continua, se vuelven a ejecutar cuando cambiamos un archivo e incluso podemos probar nuestro código en varios navegadores al mismo tiempo.

¿Qué más podríamos desear? Bueno , para probar nuestro código front-end.

Las pruebas de front-end requieren más que solo pruebas unitarias

Las pruebas unitarias son excelentes: es la mejor manera de ver si un algoritmo hace lo correcto cada vez, o para verificar nuestra lógica de validación de entrada, o transformaciones de datos, o cualquier otra operación aislada. Las pruebas unitarias son perfectas para los fundamentos.

Pero el código front-end no se trata de manipular datos. Se trata de eventos de usuario y de mostrar las vistas correctas en el momento adecuado. Los front-end se tratan de los usuarios.

Esto es lo que queremos poder hacer:

  • Probar eventos de usuario de React
  • Probar la respuesta a esos eventos.
  • Asegúrese de que las cosas correctas se rendericen en el momento correcto
  • Ejecutar pruebas en muchos navegadores
  • Vuelva a ejecutar las pruebas en los cambios de archivos
  • Trabaja con sistemas de integración continua como Travis

En los diez años que he estado haciendo esto, no había encontrado una forma decente de probar la interacción del usuario y ver la representación hasta que comencé a hurgar en React.

Pruebas unitarias de React: componentes de la interfaz de usuario

React es la forma más fácil de lograr estos objetivos. En parte, por la forma en que nos obliga a diseñar aplicaciones usando patrones comprobables, en parte, porque hay fantásticas utilidades de prueba de React.

Si nunca ha usado React antes, debe consultar mi libro React+d3.js . Está orientado a las visualizaciones, pero me dijeron que es "una introducción liviana increíble" para React.

React nos obliga a construir todo como "componentes". Puede pensar en los componentes de React como widgets o como fragmentos de HTML con algo de lógica. Siguen muchos de los mejores principios de la programación funcional, excepto que son objetos.

Por ejemplo, dado el mismo conjunto de parámetros, un componente React siempre generará el mismo resultado. No importa cuántas veces se renderice, no importa quién lo renderice, no importa dónde coloquemos la salida. Siempre lo mismo. Como resultado, no tenemos que realizar un andamiaje complejo para probar los componentes de React. Solo se preocupan por sus propiedades, no se requiere seguimiento de variables globales y objetos de configuración.

Logramos esto en gran parte evitando el estado. Llamarías a esto transparencia referencial en programación funcional. No creo que haya un nombre especial para esto en React, pero los documentos oficiales recomiendan evitar el uso del estado tanto como sea posible.

Cuando se trata de probar las interacciones de los usuarios, React nos cubre con eventos vinculados a devoluciones de llamadas de funciones. Es fácil configurar espías de prueba y asegurarse de que un evento de clic llame a la función correcta. Y debido a que los componentes de React se representan solos, podemos activar un evento de clic y verificar si hay cambios en el HTML. Esto funciona porque un componente de React solo se preocupa por sí mismo. Un clic aquí no cambia las cosas allá . Nunca tendremos que lidiar con un nido de controladores de eventos, solo llamadas a funciones bien definidas.

Ah, y como React es mágico, no tenemos que preocuparnos por el DOM. React usa el llamado DOM virtual para representar componentes en una variable de JavaScript. Y una referencia al DOM virtual es todo lo que necesitamos para probar los componentes de React, de verdad.

Es bastante dulce.

TestUtils de React

React viene con un conjunto de TestUtils . Incluso hay un corredor de pruebas recomendado llamado Jest, pero no me gusta. Explicaré por qué en un momento. Primero, los TestUtils .

Los obtenemos haciendo algo como require('react/addons').addons.TestUtils . Este es nuestro punto de entrada para probar las interacciones del usuario y verificar el resultado.

React TestUtils nos permite renderizar un componente React colocando su DOM en una variable, en lugar de insertarlo en una página. Por ejemplo, para renderizar un componente React, haríamos algo como esto:

 var component = TestUtils.renderIntoDocument( <MyComponent /> );

Luego podemos usar TestUtils para verificar si se procesaron todos los elementos secundarios. Algo como esto:

 var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' );

findRenderedDOMComponentWithTag hará lo que parece: revisar los elementos secundarios, encontrar el componente que estamos buscando y devolverlo. El valor devuelto se comportará como un componente React.

Luego podemos usar getDOMNode() para acceder al elemento DOM sin procesar y probar sus valores. Para verificar que una etiqueta h1 en el componente diga "Un título" , escribiríamos esto:

 expect(h1.getDOMNode().textContent) .toEqual("A title");

En conjunto, la prueba completa se vería así:

 it("renders an h1", function () { var component = TestUtils.renderIntoDocument( <MyComponent /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' ); expect(h1.getDOMNode().textContent) .toEqual("A title"); });

Lo bueno es que TestUtils también nos permite activar eventos de usuario. Para un evento de clic, escribiríamos algo como esto:

 var node = component .findRenderedDOMComponentWithTag('button') .getDOMNode(); TestUtils.Simulate.click(node);

Esto simula un clic y activa cualquier oyente potencial, que debería ser un método componente que cambie la salida, el estado o ambos. Esos oyentes pueden llamar a una función en un componente principal si es necesario.

Todos los casos son fáciles de probar: el estado modificado está en component.state , podemos acceder a la salida con funciones DOM normales y llamadas a funciones con espías.

¿Por qué no broma?

La documentación oficial de React recomienda usar https://facebook.github.io/jest/ como corredor de prueba y marco de prueba de React. Jest se basa en Jasmine y usa la misma sintaxis. Además de todo lo que obtienes de Jasmine, Jest también se burla de todo excepto del componente que estamos probando. Esto es fantástico en teoría, pero lo encuentro molesto. Cualquier cosa que no hayamos implementado todavía, o que provenga de una parte diferente de la base de código, simplemente no está undefined . Si bien esto está bien en muchos casos, puede conducir a fallas silenciosas.

He tenido problemas para probar un evento de clic, por ejemplo. No importa lo que intentara, simplemente no llamaría a su oyente. Luego me di cuenta de que Jest se burló de la función y nunca me dijo esto.

Pero la peor ofensa de Jest, por mucho, solía ser que no tenía un modo de reloj para probar automáticamente los nuevos cambios. Podríamos ejecutarlo una vez, obtener los resultados de las pruebas, y eso es todo. (Me gusta ejecutar mis pruebas en segundo plano mientras trabajo. De lo contrario, me olvido de ejecutarlas). Hoy en día esto ya no es un problema.

Ah, y Jest no admite la ejecución de pruebas de React en varios navegadores. Este es un problema menor de lo que solía ser, pero siento que es una característica importante para esa rara ocasión en que un heisenbug solo ocurre en una versión específica de Chrome...

Nota del editor: desde que este artículo se escribió originalmente, Jest ha mejorado sustancialmente. Puede leer nuestro tutorial más reciente, React Unit Testing Using Enzyme and Jest, y decidir por sí mismo si las pruebas Jest están a la altura hoy en día.

Pruebas de reacción: un ejemplo integrado

De todos modos, hemos visto cómo debería funcionar en teoría una buena prueba de React front-end. Pongámoslo en práctica con un breve ejemplo.

Vamos a visualizar diferentes formas de generar números aleatorios usando un componente de diagrama de dispersión hecho con React y d3.js. El código y su demostración también están en Github.

Vamos a utilizar Karma como corredor de pruebas, Mocha como marco de pruebas y Webpack como cargador de módulos.

La puesta en marcha

Nuestros archivos fuente irán a un directorio <root>/src , y pondremos las pruebas en un directorio <root>/src/__tests__ . La idea es que podamos colocar varios directorios dentro de src , uno para cada componente principal y cada uno con sus propios archivos de prueba. La agrupación de código fuente y archivos de prueba como este facilita la reutilización de componentes de React en diferentes proyectos.

Con la estructura de directorios en su lugar, podemos instalar dependencias como esta:

 $ npm install --save-dev react d3 webpack babel-loader karma karma-cli karma-mocha karma-webpack expect

Si algo no se instala, intente volver a ejecutar esa parte de la instalación. NPM a veces falla en formas que desaparecen en una nueva ejecución.

Nuestro archivo package.json debería tener este aspecto cuando hayamos terminado:

 // package.json { "name": "react-testing-example", "description": "A sample project to investigate testing options with ReactJS", "scripts": { "test": "karma start" }, // ... "homepage": "https://github.com/Swizec/react-testing-example", "devDependencies": { "babel-core": "^5.2.17", "babel-loader": "^5.0.0", "d3": "^3.5.5", "expect": "^1.6.0", "jsx-loader": "^0.13.2", "karma": "^0.12.31", "karma-chrome-launcher": "^0.1.10", "karma-cli": "0.0.4", "karma-mocha": "^0.1.10", "karma-sourcemap-loader": "^0.3.4", "karma-webpack": "^1.5.1", "mocha": "^2.2.4", "react": "^0.13.3", "react-hot-loader": "^1.2.7", "react-tools": "^0.13.3", "webpack": "^1.9.4", "webpack-dev-server": "^1.8.2" } }

Después de alguna configuración, podremos ejecutar pruebas con npm test o karma start .

Ejecución de una prueba

La configuración

No hay mucho en la configuración. Tenemos que asegurarnos de que Webpack sepa cómo encontrar nuestro código y que Karma sepa cómo ejecutar las pruebas.

Colocamos dos líneas de JavaScript en un archivo ./tests.webpack.js para ayudar a Karma y Webpack a jugar juntos:

 // tests.webpack.js var context = require.context('./src', true, /-test\.jsx?$/); context.keys().forEach(context);

Esto le dice a Webpack que considere cualquier cosa con un sufijo -test como parte del conjunto de pruebas.

Configurar Karma requiere un poco más de trabajo:

 // karma.conf.js var webpack = require('webpack'); module.exports = function (config) { config.set({ browsers: ['Chrome'], singleRun: true, frameworks: ['mocha'], files: [ 'tests.webpack.js' ], preprocessors: { 'tests.webpack.js': ['webpack'] }, reporters: ['dots'], webpack: { module: { loaders: [ {test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader'} ] }, watch: true }, webpackServer: { noInfo: true } }); };

La mayoría de estas líneas son de una configuración de Karma predeterminada. Usamos browsers para decir que las pruebas deben ejecutarse en Chrome, frameworks para especificar qué marco de prueba estamos usando y singleRun para hacer que las pruebas se ejecuten solo una vez de manera predeterminada. Puede mantener karma ejecutándose en segundo plano con karma start --no-single-run .

Esos tres son obvios. Lo de Webpack es más interesante.

Debido a que Webpack maneja el árbol de dependencias de nuestro código, no tenemos que especificar todos nuestros archivos en la matriz de files . Solo necesitamos tests.webpack.js , que luego requiere todos los archivos necesarios.

Usamos la configuración del paquete web para decirle a webpack qué hacer. En un entorno normal, esta parte iría en un archivo webpack.config.js .

También le decimos a Webpack que use babel-loader para nuestros JavaScripts. Esto nos brinda todas las características nuevas y sofisticadas de ECMAScript2015 y JSX de React.

Con la configuración de webpackServer , le decimos a Webpack que no imprima ninguna información de depuración. Solo estropearía nuestra salida de prueba.

Un componente React y una prueba

Con un conjunto de pruebas en ejecución, el resto es simple. Tenemos que hacer un componente que acepte una matriz de coordenadas aleatorias y cree un elemento <svg> con un montón de puntos.

Siguiendo las mejores prácticas de prueba de React, es decir, la práctica estándar de TDD, primero escribiremos la prueba, luego el componente real de React. Comencemos con un archivo de prueba estándar en src/__tests__/ :

 // ScatterPlot-test.jsx var React = require('react/addons'), TestUtils = React.addons.TestUtils, expect = require('expect'), ScatterPlot = require('../ScatterPlot.jsx'); var d3 = require('d3'); describe('ScatterPlot', function () { var normal = d3.random.normal(1, 1), mockData = d3.range(5).map(function () { return {x: normal(), y: normal()}; }); });

Primero requerimos React, sus TestUtils, d3.js, la biblioteca expect y el código que estamos probando. Luego creamos un nuevo conjunto de pruebas con describe y creamos algunos datos aleatorios.

Para nuestra primera prueba, asegurémonos de que ScatterPlot represente un título. Nuestra prueba va dentro del bloque de describe :

 // ScatterPlot-test.jsx it("renders an h1", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( scatterplot, 'h1' ); expect(h1.getDOMNode().textContent).toEqual("This is a random scatterplot"); });

La mayoría de las pruebas seguirán el mismo patrón:

  1. Hacer.
  2. Encuentra un nodo específico.
  3. Comprobar contenidos.

Como hemos visto anteriormente, renderIntoDocument representa nuestro componente, findRenderedDOMComponentWithTag encuentra la parte específica que estamos probando y getDOMNode nos brinda acceso DOM sin formato.

Al principio nuestra prueba fallará. Para que pase, tenemos que escribir el componente que representa una etiqueta de título:

 var React = require('react/addons'); var d3 = require('d3'); var ScatterPlot = React.createClass({ render: function () { return ( <div> <h1>This is a random scatterplot</h1> </div> ); } }); module.exports = ScatterPlot;

Eso es todo. El componente ScatterPlot representa un <div> con una etiqueta <h1> que contiene el texto esperado y nuestra prueba pasará. Sí, es más largo que solo HTML, pero tengan paciencia conmigo.

Dibuja el resto del búho

Puede ver el resto de nuestro ejemplo en GitHub, como se mencionó anteriormente. Omitiremos la descripción paso a paso en este artículo, pero el proceso general es el mismo que el anterior. Sin embargo, quiero mostrarles una prueba más interesante. Una prueba que garantiza que todos los puntos de datos aparezcan en el gráfico:

 // ScatterPlot-test.jsx it("renders a circle for each datapoint", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot data={mockData} /> ); var circles = TestUtils.scryRenderedDOMComponentsWithTag( scatterplot, 'circle' ); expect(circles.length).toEqual(5); });

Igual que antes. Renderizar, encontrar nodos, comprobar el resultado. La parte interesante aquí es dibujar esos nodos DOM. Agregamos un poco de magia ScatterPlot , así:

 // ScatterPlot.jsx componentWillMount: function () { this.yScale = d3.scale.linear(); this.xScale = d3.scale.linear(); this.update_d3(this.props); }, componentWillReceiveProps: function (newProps) { this.update_d3(newProps); }, update_d3: function (props) { this.yScale .domain([d3.min(props.data, function (d) { return dy; }), d3.max(props.data, function (d) { return dy; })]) .range([props.point_r, Number(props.height-props.point_r)]); this.xScale .domain([d3.min(props.data, function (d) { return dx; }), d3.max(props.data, function (d) { return dx; })]) .range([props.point_r, Number(props.width-props.point_r)]); }, ...

Usamos componentWillMount para configurar escalas d3 vacías para los dominios X e Y , y componentWillReceiveProps para asegurarnos de que se actualicen cuando algo cambie. Luego update_d3 se asegura de establecer el domain y el range para ambas escalas.

Usaremos las dos escalas para traducir entre valores aleatorios en nuestro conjunto de datos y posiciones en la imagen. La mayoría de los generadores aleatorios devuelven números en el rango [0,1] , que es demasiado pequeño para verlo como píxeles.

Luego agregamos los puntos al método de renderizado de nuestro componente:

 // ScatterPlot.jsx render: function () { return ( <div> <h1>This is a random scatterplot</h1> <svg width={this.props.width} height={this.props.height}> {this.props.data.map(function (pos, i) { var key = "circle-"+i; return ( <circle key={key} cx={this.xScale(pos.x)} cy={this.yScale(pos.y)} r={this.props.point_r} /> ); }.bind(this))}; </svg> </div> ); }

Este código pasa por la matriz this.props.data y agrega un elemento <circle> para cada punto de datos. Sencillo.

No se desespere más por sus pruebas de interfaz de usuario, con las pruebas de componentes de React.
Pío

Si desea obtener más información sobre la combinación de React y d3.js para crear componentes de visualización de datos, esa es otra excelente razón para consultar mi libro, React+d3.js .

Pruebas automatizadas de componentes de reacción: más fácil de lo que parece

Eso es todo lo que tenemos que saber sobre cómo escribir componentes front-end comprobables con React. Para ver más pruebas de código de los componentes de React, consulte la base de código de ejemplo de prueba de React en Github, como se mencionó anteriormente.

Hemos aprendido que:

  1. React nos obliga a modularizar y encapsular.
  2. Esto hace que las pruebas de React UI sean fáciles de automatizar.
  3. Las pruebas unitarias no son suficientes para los front-end.
  4. Karma es un gran corredor de pruebas.
  5. Jest tiene potencial, pero aún no ha llegado. (O tal vez ahora lo es).

Si te ha gustado este artículo, sígueme en Twitter y deja un comentario a continuación. ¡Gracias por leer y felices pruebas de React!

Relacionado: Cómo optimizar los componentes para mejorar el rendimiento de React