Desarrollo de React.js basado en pruebas: Pruebas unitarias de React.js con Enzyme y Jest
Publicado: 2022-03-11Cualquier pieza de código que no tenga pruebas se dice que es código heredado, según Michael Feathers. Por lo tanto, una de las mejores formas de evitar la creación de código heredado es usar el desarrollo basado en pruebas (TDD).
Si bien hay muchas herramientas disponibles para las pruebas unitarias de JavaScript y React.js, en esta publicación usaremos Jest y Enzyme para crear un componente React.js con funcionalidad básica usando TDD.
¿Por qué usar TDD para crear un componente React.js?
TDD brinda muchos beneficios a su código: una de las ventajas de la alta cobertura de prueba es que permite una fácil refactorización de código mientras mantiene su código limpio y funcional.
Si ha creado un componente React.js anteriormente, se habrá dado cuenta de que el código puede crecer muy rápido. Se llena con muchas condiciones complejas causadas por declaraciones relacionadas con cambios de estado y llamadas de servicio.
Cada componente que carece de pruebas unitarias tiene un código heredado que se vuelve difícil de mantener. Podríamos agregar pruebas unitarias después de crear el código de producción. Sin embargo, podemos correr el riesgo de pasar por alto algunos escenarios que deberían haberse probado. Al crear pruebas primero, tenemos una mayor probabilidad de cubrir todos los escenarios lógicos en nuestro componente, lo que facilitaría la refactorización y el mantenimiento.
¿Cómo hacemos una prueba unitaria de un componente React.js?
Hay muchas estrategias que podemos usar para probar un componente React.js:
- Podemos verificar que se llamó a una función particular en
props
cuando se envió cierto evento. - También podemos obtener el resultado de la función de
render
dado el estado del componente actual y hacerlo coincidir con un diseño predefinido. - Incluso podemos verificar si el número de elementos secundarios del componente coincide con una cantidad esperada.
Para utilizar estas estrategias vamos a utilizar dos herramientas que vienen muy bien para trabajar con pruebas en React.js: Jest y Enzyme.
Uso de Jest para crear pruebas unitarias
Jest es un marco de prueba de código abierto creado por Facebook que tiene una gran integración con React.js. Incluye una herramienta de línea de comandos para la ejecución de pruebas similar a la que ofrecen Jasmine y Mocha. También nos permite crear funciones simuladas con una configuración casi nula y proporciona un conjunto muy bueno de comparadores que hace que las afirmaciones sean más fáciles de leer.
Además, ofrece una característica realmente agradable llamada "prueba de instantáneas", que nos ayuda a verificar y verificar el resultado de la representación del componente. Usaremos pruebas de instantáneas para capturar el árbol de un componente y guardarlo en un archivo que podamos usar para compararlo con un árbol de representación (o lo que pasemos a la función de expect
como primer argumento).
Uso de enzimas para montar componentes de React.js
Enzyme proporciona un mecanismo para montar y recorrer los árboles de componentes de React.js. Esto nos ayudará a obtener acceso a sus propias propiedades y estado, así como a sus accesorios secundarios para ejecutar nuestras afirmaciones.
Enzyme ofrece dos funciones básicas para el montaje de componentes: shallow
y de mount
. La función shallow
carga en la memoria solo el componente raíz, mientras que el mount
carga el árbol DOM completo.
Vamos a combinar Enzyme y Jest para montar un componente React.js y ejecutar afirmaciones sobre él.
Configurando nuestro entorno
Puede echar un vistazo a este repositorio, que tiene la configuración básica para ejecutar este ejemplo.
Estamos usando las siguientes versiones:
{ "react": "16.0.0", "enzyme": "^2.9.1", "jest": "^21.2.1", "jest-cli": "^21.2.1", "babel-jest": "^21.2.0" }
Crear el componente React.js usando TDD
El primer paso es crear una prueba fallida que intentará generar un componente React.js utilizando la función superficial de la enzima.
// MyComponent.test.js import React from 'react'; import { shallow } from 'enzyme'; import MyComponent from './MyComponent'; describe("MyComponent", () => { it("should render my component", () => { const wrapper = shallow(<MyComponent />); }); });
Después de ejecutar la prueba, obtenemos el siguiente error:
ReferenceError: MyComponent is not defined.
A continuación, creamos el componente que proporciona la sintaxis básica para pasar la prueba.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div />; } }
En el siguiente paso, nos aseguraremos de que nuestro componente represente un diseño de interfaz de usuario predefinido mediante la función toMatchSnapshot
de Jest.
Después de llamar a este método, Jest crea automáticamente un archivo de instantánea llamado [testFileName].snap
, que se agrega a la carpeta __snapshots__
.
Este archivo representa el diseño de la interfaz de usuario que esperamos de la representación de nuestro componente.
Sin embargo, dado que estamos tratando de hacer TDD puro , primero debemos crear este archivo y luego llamar a la función toMatchSnapshot
para que la prueba falle.
Esto puede sonar un poco confuso, dado que no sabemos qué formato usa Jest para representar este diseño.
Es posible que tenga la tentación de ejecutar primero la función toMatchSnapshot
y ver el resultado en el archivo de instantánea, y esa es una opción válida. Sin embargo, si realmente queremos usar TDD puro , debemos aprender cómo se estructuran los archivos de instantáneas.
El archivo de instantánea contiene un diseño que coincide con el nombre de la prueba. Esto significa que si nuestra prueba tiene esta forma:
desc("ComponentA" () => { it("should do something", () => { … } });
Deberíamos especificar esto en la sección de exportaciones: Component A should do something 1
.
Puede leer más sobre las pruebas de instantáneas aquí.
Entonces, primero creamos el archivo MyComponent.test.js.snap
.
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input type="text" /> </div>, ] `;
Luego, creamos la prueba unitaria que verificará que la instantánea coincida con los elementos secundarios del componente.

// MyComponent.test.js ... it("should render initial layout", () => { // when const component = shallow(<MyComponent />); // then expect(component.getElements()).toMatchSnapshot(); }); ...
Podemos considerar components.getElements
como el resultado del método render.
Pasamos estos elementos al método expect
para ejecutar la verificación contra el archivo de instantánea.
Después de ejecutar la prueba obtenemos el siguiente error:
Received value does not match stored snapshot 1. Expected: - Array [ <div> <input type="text” /> </div>, ] Actual: + Array []
Jest nos dice que el resultado de component.getElements
no coincide con la instantánea. Entonces, hacemos que esta prueba pase agregando el elemento de entrada en MyComponent
.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input type="text" /></div>; } }
El siguiente paso es agregar funcionalidad a input
ejecutando una función cuando cambia su valor. Hacemos esto especificando una función en la propiedad onChange
.
Primero debemos cambiar la instantánea para que la prueba falle.
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input onChange={[Function]} type="text" /> </div>, ] `;
Una desventaja de modificar primero la instantánea es que el orden de los accesorios (o atributos) es importante.
Jest ordenará alfabéticamente los accesorios recibidos en la función de expect
antes de verificarlos con la instantánea. Entonces, debemos especificarlos en ese orden.
Después de ejecutar la prueba obtenemos el siguiente error:
Received value does not match stored snapshot 1. Expected: - Array [ <div> onChange={[Function]} <input type="text”/> </div>, ] Actual: + Array [ <div> <input type=”text” /> </div>, ]
Para hacer que esta prueba pase, simplemente podemos proporcionar una función vacía a onChange
.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={() => {}} type="text" /></div>; } }
Luego, nos aseguramos de que el estado del componente cambie después de enviar el evento onChange
.
Para hacer esto, creamos una nueva prueba unitaria que llamará a la función onChange
en la entrada pasando un evento para imitar un evento real en la interfaz de usuario.
Luego, verificamos que el estado del componente contenga una clave llamada input
.
// MyComponent.test.js ... it("should create an entry in component state", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toBeDefined(); });
Ahora obtenemos el siguiente error.
Expected value to be defined, instead received undefined
Esto indica que el componente no tiene una propiedad en el estado llamado input
.
Hacemos que la prueba pase configurando esta entrada en el estado del componente.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => {this.setState({input: ''})}} type="text" /></div>; } }
Luego, debemos asegurarnos de que se establezca un valor en la nueva entrada de estado. Obtendremos este valor del evento.
Entonces, creemos una prueba que asegure que el estado contenga este valor.
// MyComponent.test.js ... it("should create an entry in component state with the event value", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toEqual('myValue'); }); ~~~ Not surprisingly, we get the following error. ~~ Expected value to equal: "myValue" Received: ""
Finalmente, hacemos que esta prueba pase obteniendo el valor del evento y estableciéndolo como el valor de entrada.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => { this.setState({input: event.target.value})}} type="text" /></div>; } }
Después de asegurarnos de que todas las pruebas pasan, podemos refactorizar nuestro código.
Podemos extraer la función pasada en onChange
prop a una nueva función llamada updateState
.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { updateState(event) { this.setState({ input: event.target.value }); } render() { return <div><input onChange={this.updateState.bind(this)} type="text" /></div>; } }
Ahora tenemos un componente React.js simple creado con TDD.
Resumen
En este ejemplo, intentamos usar TDD puro siguiendo cada paso escribiendo la menor cantidad de código posible para fallar y pasar las pruebas.
Algunos de los pasos pueden parecer innecesarios y podemos tener la tentación de omitirlos. Sin embargo, siempre que nos saltemos algún paso, terminaremos usando una versión menos pura de TDD.
El uso de un proceso TDD menos estricto también es válido y puede funcionar bien.
Mi recomendación para ti es que no te saltes ningún paso y no te sientas mal si te resulta difícil. TDD es una técnica que no es fácil de dominar, pero definitivamente vale la pena hacer.
Si está interesado en obtener más información sobre TDD y el desarrollo basado en el comportamiento relacionado (BDD), lea Your Boss Won't Apreciate TDD por el compañero Toptaler Ryan Wilcox.