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.
El codigo inbluido en este articulo es absolutamente ilegible. Ojala arreglen ese problema pronto.