Da potencia y flexibilidad a tus tests con Jest

El testing es uno de los conceptos más core de eXtremme Programming (XP). Ya lo afirmaba el enorme Kent Beck:

Any program feature without an automated test simply doesn’t exist

Curiosamente, JavaScript ha sido históricamente uno de los lenguajes con más frameworks de test y menos cultura de testing en su comunidad. Los frameworks han ido apareciendo y desapareciendo a la velocidad del rayo y, al fin el día de hoy, podemos decir que tenemos un espléndido ecosistema para efectuar pruebas automáticas que ha venido para quedarse.

En este artículo hablaremos sobre Jest, con él que podemos edificar tests unitarios trabajando con matchers adaptados, crear mocks o bien revisar snapshots de componentes visuales como algo fácil y alcanzable.

Instalación y puesta en marcha

Para llenar un ciclo de retroalimentación veloz y con la máxima información en todos y cada instante, debemos escoger un framework de testing flexible, veloz y con un output fácil y entendible. Este es el caso de Jest que, basado en Jasmine, resalta por sus funcionalidades potentes y también renovadoras.

Jest ha sido desarrollado por el equipo de Fb y, si bien nace en el contexto de React, es un framework de testing generalista que podemos emplear en cualquier situación. En el momento en que comencemos con Jest, ya no desearéis mudar :)

Empezado por el principio, es esencial conocer la página primordial de Jest , la que es una de las mejoras guías que hallaréis. En ella vamos a poder localizar todo género de ejemplos y documentación que nos va a ayudar mucho a ahondar en el framework. En todo caso, lo primero es lo primero y para iniciar debemos instalarlo.

Como ocurre con cualquier otro bulto JavaScript, podemos incorporarlo a través de NPM o bien Yarn a nuestro proyecto:

npm install –save-dev jest
yarn add –dev jest

Si vamos a usar Jest con ES6 , entonces precisamos ciertas dependencias extra (partimos de la suposición de que vamos a emplear Babel seis para este caso):

npm install –save-dev jest babel-jest babel-core regenerator-runtime babel-preset-env
yarn add –dev jest babel-jest babel-core regenerator-runtime babel-preset-env

Caso de que deseemos trabajador con React y procesar JSX, entonces agregaremos asimismo el preset pertinente (recordad la nueva administración de presets que babel incorpora en sus últimas versiones):

npm install –save-dev jest babel-jest babel-core regenerator-runtime babel-preset-env babel-preset-react
yarn add –dev jest babel-jest babel-core regenerator-runtime babel-preset-env babel-preset-react

Para finalizar, definiremos en el .babelrc los presets necesarios:

undefined

NOTA: En un caso así, el preset de React se agrega solamente por si acaso deseáis a futuro examinar ciertas peculiaridades avanzadas de Jest como el *snapshot testing, mas no sería verdaderamente obligatorio para el propósito de este artículo.*

Con todo el tooling preparado, que no es poco, si deseamos ejecutar los tests desde NPM agregaremos una nueva entrada en la sección de scripts del archivo package.json de nuestro proyecto:

undefined

De este modo, al ejecutar npm test (o bien npm t si te van los shortcuts), se invocará Jest y se ejecutarán nuestros tests:

Como es natural, la ejecución falla al no localizar ningún test aún en el proyecto, mas esto va a mudar prontísimo :)

Para ir calentando, crearemos un caso un tanto chorra que nos deje revisar que todo marcha apropiadamente a nivel de configuración del proyecto.

Para esto, vamos a crear un carpetita test donde guardar nuestras pruebas automáticas y dentro vamos a crear un archivo sum.test.js con el próximo contenido:

test('should sum two numbers', () => undefined);

No hay demasiado que explicar acá, verdad??? :)

Vamos puesto que a ejecutar los tests nuevamente y de este modo nos marchamos familiarizando con la salida:

Nuestro primer verde!! Ahora no vamos a poder parar.

En los próximos apartados analizaremos como getionar las diferentes fases de definición de un test: Arrange, Act y Assert. En el Arrange estableceremos el estado inicial del que partimos (setup/tearDown), entonces ejecutaremos en el Act el código que altera ese estado incial y, finalmente, realizaremos las comprobaciones precisas en el Assert usando los matchers que Jest nos provee o bien, aun, definiendo los nuestro propios.

Definición de contextos

Como articular nuestros tests es un aspecto esencial que condiciona su mantenibilidad conforme medra el proyecto y el número de tests que tenemos. En general, afirmamos que los tests se reúnen en suites, que no son más que agrupaciones de tests que están relacionados. Personalmente, prefiero que la causa de agrupación de las pruebas sea un contexto común, un arrange compartido en el contexto de la funcionalidad que abordamos. Es en esta clase de situaciones donde verdaderamente vamos a ver de qué manera interactuan las funcionalidades que se relacionan cohesivamente.

Para la definición de esta clase de contextos, Jest nos da los próximos niveles de agrupación:

Nivel de archivo. Cada suite puede ir en un archivo diferente siempre que sea detectado por Jest. Esto sucede bajo determinadas condiciones como que se llame *.test.js o bien que esté en un directorio __test__, por servirnos de un ejemplo.
Definición de contextos Podemos reunir tests a través de la construcción describe bajo un término compartido. Ejemplo:
describe('User registration', () => undefined);

Los contextos o bien describe son anidables, si bien en general no se aconseja más de 2 niveles de anidamiento puesto que complican mucho la legibilidad del conjunto.

Gestión del estado inicial del que partimos: Setup y TearDown

Para la ejecución de una prueba automática es preciso establecer antes de seguir un escenario concreto que reproduca el estado del que deseamos partir. Para esto, podemos tener ya código especializado que inicia ciertas tablas en una base de datos, crea unos archivos en el sistema o bien inicia ciertas estructuras de datos. Sea como fuere, es preciso que nuestro framework nos proveea de determinados métodos que se ejecutarán siempre y en todo momento ya antes y tras cada test. Es el caso de Jest, estos son beforeEach y afterEach:

beforeEach(() => undefined);

afterEach(() => undefined);

test('Car should be present in the catalog', () => undefined);

Posiblemente tanto en beforeEach como en afterEach debamos efectuar llamadas a código asíncrono (conexión a BD, llamada a servicio o bien afín). Por esta razón, estas funciones son capaces de percibir promesas a resultas de su invocación, de manera que Jest aguardará a que la promesa se complete ya antes de ejecutar el test. El único punto a tener en consideración es que la promesa se debe devolver siempre y en toda circunstancia como resultado usando return:

beforeEach(() => undefined);

Finalmente, si la inicialización o bien limpieza que debemos efectuar solamente se genera al comienzo de lanzar toda la suite y al final, vamos a poder usar las expresiones beforeAll y afterAll:

beforeAll(() => undefined);

afterAll(() => undefined);

test('Car should be present in the catalog', () => undefined);

test('Motorbike should be present in the catalog', () => undefined);

Por otro lado, beforeEach y afterEach se pueden usar en un contexo definido con describe y, en un caso así, solamente se aplicarían a los tests que están definidos en ese contexto:

Con la intención de podamos entender la secuencia específica de métodos libres y como se marchan ejecutando en Jest, podemos ver el output de la próxima secuencia de definiciones:

beforeAll(() => console.log('1 – beforeAll'));
afterAll(() => console.log('1 – afterAll'));
beforeEach(() => console.log('1 – beforeEach'));
afterEach(() => console.log('1 – afterEach'));
test('', () => console.log('1 – test'));
describe('Scoped / Nested block', () => undefined);

// 1 – beforeAll
// 1 – beforeEach
// 1 – test
// 1 – afterEach
// dos – beforeAll
// 1 – beforeEach
// dos – beforeEach
// dos – test
// dos – afterEach
// 1 – afterEach
// dos – afterAll
// 1 – afterAll

Matchers

Los matchers son las funciones usadas por el framework de test para revisar si el valor aguardado por la prueba automática coincide verdaderamente con el conseguido. Obviamente, si el valor no coincide el test va a fallar (FAIL) y nos enseñará, marcando la salida en colorado, como ha sido la discrepancia de valores. Por otro lado, si valor aguardado coincide con el logrado, vamos a tener ese adictivo verde (PASS) que nos va a llevar a redactar el próximo test.

Es precisamente por esto que, cuando equiparamos 2 valores al final de un test, es esencial lograr que no se pierda la pretensión que deseamos expresar con esta prueba automática. Recordemos que los tests son la documentación ejecutable de nuestro proyecto y, si esta no es comprensible y fácil, distinguir lo que verdaderamente el software va a ser mucho más difícil y, en consecuencia, deberemos invertir considerablemente más tiempo en su mantenimiento y probablemente acabaremos cometiendo considerablemente más fallos.

Cuando equiparamos 2 valores al final de un test, es esencial lograr que no se pierda la pretensión que deseamos expresar con esta prueba automática

Si partimos de la base, nuestro test en su estructura básica de Arrange, Act y Assert habrá de ser veloz de leer y fácil de comprender. No hay nada peor que encontrarse con un test en colorado, leerlo y encontrarse con una definición compleja que no somos capaces de digerir en pocos segundos.

Para eludir esta situación y lograr que nuestros tests sean verdaderamente semánticos, el naming es esencial. Emplear buenos nombres de definición de nuestros tests, buenos nombres de variables, aseveraciones de negocio (ya vamos a ver más adelante que son) y cualquier otra técnica que mejore la expresividad, nos salvará la vida cuando más lo precisemos.

Mas bueno, todo esto va a llegar poquito a poco y, si verdaderamente interiorizamos estos principios, vamos a poder ir mejorando nuestra base de test ganando de este modo velocidad de cambio en el futuro.

Para ir avanzado, podemos partir de lo básico que nos ofrece Jest por defecto para equiparar tipos primitivos como son cadenas, valores numéricos o bien compilaciones, dejando para la próxima sección la definición de comparaciones o bien matchers adaptados. Comparaciones disponibles:

Igualdad

Ya sabemos que la igualdad en JavaScript es un tema frágil, con lo que en este comparador lo mejor es aclarar que se va a aplicar como se define en el estándar para Object.is (si no conocéis esta función, os aconsejo explorar este y otros métodos de Object que en muchas ocasiones obviamos en el momento de redactar código):

expect(tres dos).toBe(cinco);

Si en vez de revisar igualdad deseamos contrastar lo opuesto, podemos proseguirse usando el comparador toBe antecedido de not. Personalmente me chifla esta aproximación desde la perspectiva de la semántica extra que agrega, puesto que creo que se semeja considerablemente más al lenguaje natural y facilita su lectura cuando se equipara con otras aproximaciones de otros frameworks.

expect(tres tres).not.toBe(cinco);

Finalmente con las comparaciones de igualdad, no podíamos dejarnos fuera de la ecuación los valores que en JavaScript pueden ser truthy o bien falsy. Es precisamente por esto que en Jest tenemos soporte para cualquier comparación del estilo:

expect(null).toBeNull();
expect(null).toBeDefined();
expect(null).not.toBeUndefined();
expect(null).not.toBeTruthy();
expect(null).toBeFalsy();

Objetos y listas

Para el caso de estructuras complejas como son los objetos y las listas, incluidas aquellas que pueden presentar diferentes niveles de profundidad, el operador conveniente no es tanto toBe sino más bien toEqual, al permitir una comparación profunda o bien deep equality que evalua apropiadamente la estructura completa:

let data = undefined;
data['two'] = 2;

expect(data).toEqual(undefined);
expect(data).toEqual(undefined);

En el caso específico de las listas, algo bastante frecuente es el poder revisar si el resultado de una operación que devuelve algún género de compilación contiene el valor buscado. Para esto, usaremos el matcher toContain, que nos deja explorar esta compilación sin iterar sobre ella y sin usar funciones como indexOf o bien includes en la colección:

expect(['cat', 'beer', 'dog']).toContain('beer');

Valores numéricos

Ya fuera de los