0

Testing de componentes de React con Jest

1733Puntos

hace 4 años

ReactJS se ha convertido en una de las librerías más populares para crear las interfaces de usuario de nuestras aplicaciones. Introdujo un nuevo concepto llamado componentes; y, debido a esto, los mismos creadores de Facebook vieron la necesidad de encontrar una herramienta para poder hacer tests efectivos de React. Y así crearon Jest. Se trata de un framework para Unit Testing basado en Jasmine. Pero con muchas más funcionalidades:
  • Hace de manera automática mocks de las dependencias CommonJS: puedes declarar módulos require("modulo") en cualquier sección de tus tests.
  • Encuentra y ejecuta de manera automática los tests: al ejecutar el comando Jest, buscará las carpetas tests en tu proyecto y ejecutará todo lo que se encuentre ahí. Además, puedes configurar el nombre de esa carpeta.
  • Ejecuta los tests en una implementación falsa del DOM (usando JsDOM) para poder correr los tests desde la consola sin necesidad de hacerlo en el navegador.

Instalación

Para poder testear código ES6 debemos de tener instalado Babel global [js] npm i -g babel [/js] Después, instalamos el cliente de Jest para poder ejecutar los test desde la terminal [js] npm i -g jest-cli [/js] Luego, instalamos en local las dependencias necesarias para React y Jest [js] npm i -D babel-jest jest-cli react [/js]

Configuración

En el package.json debemos configurar Jest de la siguiente manera: [js] "jest": { "scriptPreprocessor": "<rootDir>/node_modules/babel-jest", "testFileExtensions": [ "es6", "js" ], "moduleFileExtensions": [ "js", "json", "es6", "jsx" ], "unmockedModulePathPatterns": ["<rootDir>/node_modules/react"] } [/js]
  • rootDir: es el folder donde jest buscará los tests y los módulos por default. También es la carpeta que contiene el package.json por defecto.
  • scriptPreprocessor: definimos que pre-procesador va a ejecutar los tests. En nuestro caso utilizamos babel-jest para poder interpretar ES6.
  • testFileExtensions: definimos los tipos de archivos en los que pueden estar escritos los test.
  • moduleFileExtensions: definimos los tipos de archivos que los tests pueden interpretar.
Si te interesa conocer más acerca de su configuración, puedes encontrar más opciones en su API.

Jest en acción

El componente que vamos a testear para el ejemplo será TODO.jsx. Verificaremos que su funcionamiento sea correcto. [js] import React from 'react/addons'; const TODO = React.createClass({ propType: { defaultText: React.PropTypes.string, }, getDefaultProps() { return { defaultText: "task", }; }, getInitialState() { return { items: [], defaultText: this.props.defaultText, }; }, onKeyDown(e) { e.preventDefault(); if (e.keyCode == 13) { const textItem = this.refs.item.getDOMNode().value.trim(); const isEmpty = (textItem.length === 0); //this.refs.item.getDOMNode().value = ''; const item = { text: textItem, }; const {items} = this.state; if (!isEmpty) { items.push(item); this.setState({items}); } } }, onChange(e) { e.preventDefault(); this.setState({ defaultText: this.refs.item.getDOMNode().value.trim(), }); }, onDeleteItem(itemText) { const items = this.state.items.filter((item) => item.text != itemText); this.setState({items}); }, renderList() { return this.state.items.map((item, index) => { return ( <li className="TODO-Item" key={index}> <span className="TODO-ItemDeleteIcon" onClick={() => {this.onDeleteItem(item.text)}} > x </span> {item.text} </li>); }); }, render() { const list = this.renderList(); return ( <div className="TODO"> <form className="TODO-Form"> <input type="text" onChange={this.onChange} onKeyDown={this.onKeyDown} value={this.state.defaultText} ref="item" /> <button type="button" onClick={this.onClick}> enviar</button> </form> <ul className="TODO-List"> {list} </ul> </div> ); }, }); export default TODO; [/js] Los casos a testear serán:
  • El componente debe estar definido.
  • El input debe existir y estar definido como elemento del DOM.
  • Al presionar enter se debería de crear un ítem.
  • Al dar clic sobre el ícono de borrado de un ítem, este debería ser eliminado.
  • Si presionamos enter para crear un ítem pero el input esta vacío, no debería de crear elemento alguno.
  • La cantidad de ítems debería ser igual que la cantidad de ítems en el state.

Caso 1: El componente debe estar definido

Para poder simular los componentes necesitaremos usar la herramienta TestUtils de React. La podremos usar al importar react/addons. [js] import React from 'react/addons'; const {TestUtils} = React.addons; [/js] TestUtils tiene el método renderIntoDocument. Este nos permitirá renderizar componentes y generar un DOM "falso" con JsDOM; al que le podemos pasar atributos (props). [js] const TodoComponent = TestUtils.renderIntoDocument(<TODO defaultText="new task"/>); [/js] TestUtils cuenta con diferentes métodos para definir el estado de un componente o un elemento del DOM:
  • isCompositeComponent: recibe el elemento y retorna true si este es un componente de React.
  • isDOMComponent: recibe elementos y retorna true si este pertenece al DOM.
El test para este caso, se debería ver así: [js] import React from 'react/addons'; const {TestUtils} = React.addons; jest.dontMock('../TODO.jsx'); describe('TODO', () => { //definimos el componente a Testear const TODO = require('../TODO.jsx'); const TodoComponent = TestUtils.renderIntoDocument(<TODO defaultText="new task"/>); //con TestUtils renderizamos un componente al cual le podemos pasar datos it("El componente debe estar definido", () => { expect(TestUtils.isCompositeComponent(TodoComponent)).toBeTruthy(); }); }); [/js] Usamos jest.dontMock('../TODO.jsx'); porque Jest hace mock de cada componente. En este caso estamos usando el componente real.

Caso 2: El input debe existir y estar definido como elemento del DOM

Para poder encontrar elementos del DOM podemos usar los siguientes métodos de TestUtils:
  • findRenderedDOMComponentWithTag: permite encontrar un elemento del DOM con el tag HTML que se envíe. Por ejemplo, < span > o < h1 >
  • scryRenderedDOMComponentsWithTag: es igual que findRenderedDOMComponentsWithTag, pero retorna un array con todos los elementos con este tag.
  • findRenderedDOMComponentWithClass: permite encontrar un elemento del DOM por su clase.
  • scryRenderedDOMComponentsWithClass: Es igual que findRenderedDOMComponentsWithClass pero en este caso retorna un array con todos los elementos con esta clase.
Ahora usaremos el método findRenderedDOMComponentWithTag para encontrar el input. En este caso, se vería así: [js] it('El input debe existir y estar definido como elemento del DOM', () => { const input = TestUtils.findRenderedDOMComponentWithTag(TodoComponent, 'input'); expect(TestUtils.isDOMComponent(input)).toBeTruthy(); }); [/js]

Caso 3: Al presionar enter se debería de crear un ítem

TestUtils tiene un método que nos será muy útil: Simulate. Este puede emular un evento JavaScript en nuestro componente a testear. Puede ser click, keyDown, chagne, hover, etc. Simulate recibe como parámetros el elemento del DOM al que queramos ejercerle la acción y, de manera opcional, eventData. Este caso se vería así: [js] it('al enviar el formulario se deberia de crear un item', () => { //definimos los componentes del DOM const input = TestUtils.findRenderedDOMComponentWithTag(TodoComponent, 'input'); //Simulamos cambio en el input TestUtils.Simulate.change(input); //Simulamos el keDown de la letra #13 de enter TestUtils.Simulate.keyDown(input, {key: "Enter", keyCode: 13, which: 13}); //al crear un item, deberia de renderizarse otro elemento de la lista, por ende, //aumentar el largo del array de este const items = TestUtils.scryRenderedDOMComponentsWithClass(TodoComponent, 'TODO-Item'); //al crear una tarea, verificamos que el largo sea mayor que cero expect(items.length > 0).toBeTruthy(); }); [/js]

Caso 4: Al dar clic sobre el ícono de borrado de un ítem, este debería ser eliminado

Ahora usaremos un evento click con Simulate. Se debería ver así: [js] it('Al presionar click sobre el icono de borrado de un item este deberia ser eliminado', () => { const Todo = TestUtils.renderIntoDocument(<TODO defaultText="task"/>); const input = TestUtils.findRenderedDOMComponentWithTag(Todo, 'input'); //Simulamos la creación de un item. TestUtils.Simulate.change(input); TestUtils.Simulate.keyDown(input, {key: "Enter", keyCode: 13, which: 13}); //buscamos todos los elementos con clase TODO-ItemDeleteIcon. const deleteIcons = TestUtils.scryRenderedDOMComponentsWithClass(Todo, 'TODO-ItemDeleteIcon'); //como solo creamos uno, entonces tomamos este y emulamos el evento click TestUtils.Simulate.click(deleteIcons[0]); //ahora traemos todos los items, no deberia de haber ninguno const items = TestUtils.scryRenderedDOMComponentsWithClass(Todo, 'TODO-Item'); //como solo creamos uno y borramos uno, el array de items deberia tener de largo cero expect(items.length === 0).toBeTruthy(); }); [/js]

Caso 5: Si presionamos enter para crear un ítem, pero el input esta vacío, no debería crear elemento alguno

Para este caso verificamos que, luego de haber intentado crear un ítem con el input vacío, el largo de los ítems sea igual a cero. [js] it('Si al enviar el formulario este esta vacio no deberia crear la tarea', () => { const Todo = TestUtils.renderIntoDocument(<TODO defaultText=""/>); const input = TestUtils.findRenderedDOMComponentWithTag(Todo, 'input'); TestUtils.Simulate.change(input); //Simulamos el keDown de la letra #13 de enter TestUtils.Simulate.keyDown(input, {key: "Enter", keyCode: 13, which: 13}); const items = TestUtils.scryRenderedDOMComponentsWithClass(Todo, 'TODO-Item'); //al crear una tarea, verificamos que el largo sea mayor que cero expect(items.length === 0).toBeTruthy(); }); [/js]

Caso 6: La cantidad de ítems debería ser igual que la cantidad de ítems en el state

Como items es un array en el state de nuestro componente, debemos verificar que cuando creemos ítems, este cambie. Al encontrar un componente utilizando los distintos métodos de TestUtils, podremos acceder a este y a todos sus métodos y atributos. En este caso accederemos a su state. [js] it('La cantidad de items deberia de ser igual que la cantidad de items en el state', () => { const input = TestUtils.findRenderedDOMComponentWithTag(TodoComponent, 'input'); for (let i = 0; i < 10; i++) { TestUtils.Simulate.change(input); //Simulamos el keDown de la letra #13 de enter TestUtils.Simulate.keyDown(input, {key: "Enter", keyCode: 13, which: 13}); } const items = TestUtils.scryRenderedDOMComponentsWithClass(TodoComponent, 'TODO-Item'); expect(TodoComponent.state.items.length === items.length).toBeTruthy(); }); [/js] Esto es sólo una pequeña parte de todo lo que podemos hacer con Jest y TestUtils. Si quieres aprender más acerca de estas herramientas y comenzar a desarrollar aplicaciones modulares con la librería de Facebook, regístrate hoy al curso de React.js en Platzi. Entrar a curso de React.js
Jeison
Jeison
@json

1733Puntos

hace 4 años

Todas sus entradas
Escribe tu comentario
+ 2
1
8353Puntos

El codigo inbluido en este articulo es absolutamente ilegible. Ojala arreglen ese problema pronto.