Caracterizando una aplicación creando pruebas automatizadas a través de Cypress y Cucumber

Gonzalo Pincheira A.
Angular Chile
Published in
12 min readMay 18, 2019

Cuando trabajamos en un software ya existente (código legado) seguramente podremos observar un montón de funcionalidades tal y como el denso bosque de la imagen de portada de este artículo. A medida que nos vamos acercando descubriremos que otros caminos se esconden en sus rincones.

En un software legado podemos observar a primera vista ciertas estructuras, textos, acciones, etc . que representan el camino que vemos en la imagen y esa es la base que nos va a servir como punto de partida para poder hacer pruebas automatizadas para asegurar que el comportamiento actual se mantenga en el tiempo.
Una técnica que nos permite poder especificar los caminos básicos de nuestro software para lograr la funcionalidad para la que fue construido es la “Caracterización del Software” y podemos hacer esto a través de pruebas automatizadas lo cuál nos entregará múltiples beneficios:

  • Ejecutar de manera automatizada los pasos requeridos para interactuar con alguna funcionalidad.
  • Capa de seguridad para que una vez que empecemos a modificar el software nos informará si rompimos algo.
  • Documentación respecto de las funcionalidades ya existentes.
  • Simular Bugs que encontremos sin necesidad de intervenir el código.

Caracterización de un software

Vamos a caracterizar la aplicación por defecto que trae Angular al utilizar el comando ng new .

Primero ejecutemos el siguiente comando para actualizar el CLI. Si ya lo tenemos este se actualizará:

npm i -g @angular/cli

Sigamos las siguientes instrucciones:

ng new angular-cypress-cucumber --routing=true --style=scss
cd angular-cypress-cucumber
npm start

Una vez ejecutado el último comando vamos desde nuestro navegador a http://localhost:4200 y veremos lo siguiente:

Aplicación por defecto.

Ahora vamos a definir con palabras lo que actualmente contiene esta página:

- La url es "localhost:4200".
- Para la url descrita anteriormente obtenemos una página con las siguientes características:
1. El título "Welcome to angular-cypress-cucumber!".
2. Un logo con el atributo de accesibilidad alt "Angular Logo".
3. Un texto informativo indicando algunos links de interés sobre Angular. El texto es "Here are some links to help you start:"
4. Un texto tipo lista con el texto "Tour of Heroes" y el atributo href "https://angular.io/tutorial"
5. Un texto tipo lista con el texto "CLI Documentation" y el atributo href "https://angular.io/cli"
6. Un texto tipo lista con el texto "Angular blog" y el atributo href "https://blog.angular.io/"

Listo! este texto que escribimos lo llamaremos “La caracterización de un software”.

Gherkin: Un lenguaje para especificar características de un software

Una vez entendiendo que significa “Caracterizar un software” ya estaremos preparados para comprender el rol que cumple Cucumber en la creación de pruebas automatizadas. Cucumber es un framework que nos permite automatizar pasos descritos a través de un “lenguaje específico de dominio” (DSL por sus siglas en inglés). Este DSL se llama Gherkin y es un lenguaje simplificado que consiste en escribir una serie de instrucciones con un prefijo o palabra clave que dará sentido a lo que se quiere automatizar.

  • Feature: Indica el nombre de la funcionalidad y una descripción sobre el comportamiento esperado.
  • Scenario: Describe un caso de negocio para la funcionalidad
  • Given: Describe las condiciones necesarias para luego poder corroborar un comportamiento, por ejemplo dirigirse a cierta url para comenzar a hacer acciones o corroborar cosas, o crear una cookie con cierto formato que es necesaria para que funcione una página, o insertar un token en el local-storage, etc.
  • When: Representa una acción o un conjunto de acciones que permitirá que ocurra algo en la prueba automatizada como por ejemplo hacer click en un botón o llenar multiples datos en un formulario.
  • Then: Representa una afirmación respecto de alguna acción que haya sido ejecutada o sobre algún valor consultado en la página actual.

Ahora apliquemos la sintaxis Gherkin a la caracterización que hicimos de nuestro software.

Feature: Página de Inicio  Esta página debe contener links de interés sobre Angular y un texto de bienvenida con el logo de Angular  Scenario: Página Inicial    Given nos dirigimos a la url de la página de inicio
Then el título de la página es "Welcome to angular-cypress-cucumber!"
Then existe un logo de Angular con el atributo de accesibilidad alt "Angular Logo"
Then existe un texto informativo "Here are some links to help you start:"
Then existe un texto tipo lista con el texto "Tour of Heroes" y el atributo href "https://angular.io/tutorial"
Then existe un texto tipo lista con el texto "CLI Documentation" y el atributo href "https://angular.io/cli"
Then existe un texto tipo lista con el texto "Angular blog" y el atributo href "https://blog.angular.io/"

Fácil no? Aún quedan una incógnita por resolver. ¿Cómo ejecutar código Javascript con Cucumber?

Primero hay que aclarar que Cucumber no es más que una implementación que permite interpretar archivos escritos con la sintaxis Gherkin y ejecutar un código Javascript asociado a alguna de las palabras clave Given, When o Then (Feature y Scenario sólo se utilizan a modo de texto descriptivo y de agrupación). Pero no basta con esto ya que el código Javascript ejecutado debe ser capaz de ejecutar acciones sobre un aplicación que está funcionando en alguna determinada url. Para esto utilizaremos una potente herramienta que en los últimos años ha irrumpido con fuerza para ayudar a los programadores a tener un flujo de trabajo para escribir pruebas automatizadas que sea fácil de usar y de configurar. Esta herramienta se llama Cypress.

Agregando Cypress y un plugin para procesar especificaciones utilizando Cucumber

Cypress es un Framework que nos permite ejecutar múltiples acciones y obtener toda la información que esté disponible en el árbol DOM de una aplicación Web. Es totalmente Agnóstico de la tecnología en la cuál hayamos escrito nuestra aplicación y provee una interfaz que se levanta automáticamente cuando corremos el comando cypress open .

Ejecutemos los siguientes comandos que nos permitirán eliminar la configuración por defecto para pruebas automatizadas que trae Angular (Protractor) y a la vez instalar Cypress y un plugin que nos permitirá utilizar Cucumber para describir la funcionalidad de la aplicación.

rm -rf e2e/*
npm uninstall --save-dev protractor
npm install --save-dev cypress cypress-cucumber-preprocessor

Cabe destacar que estas instrucciones las puedes seguir tanto para un proyecto nuevo como para uno ya existente siempre y cuando nunca hayan hecho uso de la carpeta e2e que trae la configuración por defecto para trabajar con Protractor.

Ahora modifiquemos el archivo package.json para incluir el comando de ejecución de Cypress y la definición de donde estará el código Javascript que se ejecutará por cada sentencia Given, When o Then presente en los archivos de especificación que vamos a escribir.

package.json

...
{
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "cypress open"
},
...
"cypress-cucumber-preprocessor": {
"step_definitions": "./e2e/step-definitions"
},
"dependencies": {
...

Creamos un archivo cypress.json en la raíz de la aplicación (debería quedar junto al archivo angular.json ) para decirle a Cypress que carpetas debe utilizar para la ejecución de las pruebas.

{
"integrationFolder": "./e2e/features",
"pluginsFile": "./e2e/plugins/index.js"
}

y por último vamos a crear una nueva estructura dentro de la carpeta e2e de la siguiente manera:

dentro de esta estructura debemos crear un archivo muy importante que será el encargado de decirle a Cypress como debe procesar la especificación de la prueba automatizada que vamos a escribir gracias al plugin de Cypress cypress-cucumber-preprocessor . Dentro de la carpeta plugins creamos el archivo index.js que debería tener el siguiente contenido:

./e2e/plugins/index.js

const cucumber = require('cypress-cucumber-preprocessor').default;module.exports = (on) => {
on('file:preprocessor', cucumber())
};

Listo! ahora abrimos 2 ventanas de la terminal. En la primera corremos npm start y en la segunda escribimos npm run e2e . Nunca debemos olvidar que nuestra aplicación corriendo es un proceso totalmente aparte de las pruebas automatizadas. Quedará como tarea de los programadores elegir la mejor forma de correr ambos procesos en paralelo ya sea usando herramientas npm como npm-run-all o concurrently, paquetes para Angular como ngx-cypress-builder o nwrl Nx Cypress, o simplemente como lo hicimos en este artículo que por facilidad lo haremos en 2 ventanas de la terminal separadas (Aunque las opciones anteriores deberían ser lo correcto para poder correr nuestras pruebas en ambientes de Integración Continua pero eso ya es un tema para otro artículo).

Corremos los comandos descritos anteriormente en 2 ventanas de la terminal.

y veremos aparecer una ventana automáticamente con lo siguiente:

Esto significa que ya esta todo listo para que comencemos a escribir nuestras pruebas pero primero vamos a analizar un poco más sobre Cucumber y cuál es el valor que le entrega a nuestros desarrollos.

Agregando una especificación y corriendo pruebas automatizadas con Cypress

Vamos a detener la ventana en la cuál corrimos el comando npm run e2e y vamos a crear un nuevo archivo llamado home.feature dentro de la carpeta ./e2e/features/ con el contenido de la descripción Gherkin que escribimos anteriormente.

./e2e/features/home.feature

Feature: Página de InicioEsta página debe contener links de interés sobre Angular y un texto de bienvenida con el logo de AngularScenario: Página Inicial    Given nos dirigimos a la url de la página de inicio
Then el título de la página es "Welcome to angular-cypress-cucumber!"
Then existe un logo de angular con el atributo de accesibilidad alt "Angular Logo"
Then existe un texto informativo "Here are some links to help you start:"
Then existe un texto tipo lista con el texto "Tour of Heroes" y el atributo href "https://angular.io/tutorial"
Then existe un texto tipo lista con el texto "CLI Documentation" y el atributo href "https://angular.io/cli"
Then existe un texto tipo lista con el texto "Angular blog" y el atributo href "https://blog.angular.io/"

Ahora vamos a seguir una de las convenciones de Cucumber que se define a través del plugin cypress-cucumber-preprocessor : Por cada archivo .feature existente dentro de la carpeta features se ejecutarán todos los archivos Javascript dentro de la carpeta./e2e/step-definitions .

Cabe destacar que podríamos elegir cualquier otra carpeta en la que estarán los archivos que definirán que código se ejecuta por cada feature. Esto se puede configurar en el archivo package.json como mencionamos anteriormente. (para más detalles revisar este link)

Ahora vamos a escribir un archivo llamado home.js que será relativo ahome.feature de forma de saber donde estarán descritos los pasos automatizados por cada especificación.

./e2e/step-definitions/home.js

import { Given, Then } from 'cypress-cucumber-preprocessor/steps';Given('nos dirigimos a la url de la página de inicio', () => {});Then('el título de la página es {string}', (expectedPageTitle) => {
console.log(expectedPageTitle);
});
Then('existe un logo de angular con el atributo de accesibilidad alt {string}', (expectedImgAltForLogo) => {
console.log(expectedImgAltForLogo);
});
Then('existe un texto informativo {string}', (expectedListDescriptionText) => {
console.log(expectedListDescriptionText);
});
Then('existe un texto tipo lista con el texto {string} y el atributo href {string}', (expectedListItemText, expectedListItemHref) => {
console.log(expectedListItemText, expectedListItemHref);
});

Ahora vamos a volver a ejecutar el comando npm run e2e sin olvidar de estar corriendo en otra ventana de la terminal el comando npm start .

Deberíamos ver lo siguiente:

Y al hacer click sobre home.feature deberíamos ver esto:

Finalmente podemos hacer clic derecho en cualquier parte para presionar “inspeccionar” y veremos el resultado que se muestra en la siguiente imagen.

Para entender más en detalle como funciona esto observemos la siguiente imagen. Por cada especificación en un archivo .feature se va a mapear la sentencia con la función en el archivo correspondiente. Cada frase en el archivo de especificación escrita en comillas dobles (en este ejemplo "Welcome to angular-cypress-cucumber!" ) será interpolada a través de la palabra clave {string} y su valor será asignado a los argumentos de la función que vamos a ejecutar.

De esta manera podemos utilizar esta variable en alguna prueba por ejemplo para corroborar si este valor escrito en la especificación es efectivamente el que se ve actualmente en la aplicación. ¿Y cómo hacemos para obtener esos valores desde el navegador y poder compararlo con el texto esperado?

Es aquí donde entra en acción la poderosa API de Cypress la cuál nos provee muchas maneras de hacer pruebas y obtener valores ya sean cookies, texto dentro de algún espacio de HTML, cambiar la url del navegador, hacer clics y scroll al navegador y mucho más.

Utilizando la API de Cypress para darle sentido a las pruebas automatizadas

Vamos a actualizar el archivo home.js con varias instrucciones las cuales explicaremos más adelante.

./e2e/step-definitions/home.js

import { Given, Then } from 'cypress-cucumber-preprocessor/steps';Given('nos dirigimos a la url de la página de inicio', () => {
cy.visit('http://localhost:4200');
});
Then('el título de la página es {string}', (expectedPageTitle) => {
cy.get('h1')
.invoke('text')
.should('contains', expectedPageTitle);
});
Then('existe un logo de angular con el atributo de accesibilidad alt {string}', (expectedImgAltForLogo) => {
cy.get('img')
.invoke('attr', 'alt')
.should('to.equal', expectedImgAltForLogo);
});Then('existe un texto informativo {string}', (expectedListDescriptionText) => {
cy.get('h2')
.invoke('text')
.should('contains', expectedListDescriptionText);

});
Then('existe un texto tipo lista con el texto {string} y el atributo href {string}', (expectedListItemText, expectedListItemHref) => {
const listItemIndexesMap = {
'Tour of Heroes': 1,
'CLI Documentation': 2,
'Angular blog': 3,
};
const listIndex = listItemIndexesMap[expectedListItemText];
cy.get(`li:nth-child(${listIndex})`)
.invoke('text')
.should('contains', expectedListItemText);
cy.get(`li:nth-child(${listIndex}) a`)
.invoke('attr', 'href')
.should('to.equal', expectedListItemHref);

});

Ahora vamos a explicar cada una de estas instrucciones basados en cada paso de automatización

Given('nos dirigimos a la url de la página de inicio', () => {})

Para satisfacer este paso utilizamos cy.visit() que recibe como argumento la url que cypress abrirá en el navegador. En este caso la url de la aplicación Angular http://localhost:4200.

Then('el título de la página es {string}', (expectedPageTitle) => {})
Then('existe un logo de angular con el atributo de accesibilidad alt {string}', (expectedImgAltForLogo) => {})
Then('existe un texto informativo {string}', (expectedListDescriptionText) => {})

Estos 3 pasos los resolvemos obteniendo el valor interpolando con {string} y asignándolo al primer argumento de la función donde vamos a utilizar la API de Cypress. Vamos a generar una cadena de operaciones que consistirá en:

1- Utilizar cy.get para obtener un elemento jQuery (Sí! volvió en forma de utilitario para pruebas funcionales) que representará un elemento del DOM.

2 - Utilizar .invoke que es una función que se aplica sobre el elemento jQuery y que recibe como argumento el nombre de la función que queremos aplicar sobre el elemento. Si se fijan en el código ejecutamos 2 funciones text (https://api.jquery.com/text/) y attr (https://api.jquery.com/attr/) que nos servirán para obtener valores relacionados al elemento DOM sobre el que estamos operando.

3- Utilizamos .should que ejecuta una comparación entre el valor obtenido en la operación anterior ( .invoke() ) y algún valor. Esta comparación es fundamental para dar por pasada o fallida la prueba.

Then('existe un texto tipo lista con el texto {string} y el atributo href {string}', (expectedListItemText, expectedListItemHref) => {

En este paso combinamos las operaciones utilizadas anteriormente pero dandole un poco más de lógica ya que este paso lo reutilizamos en 3 pasos (fijarse en el el archivo .feature ).

Listo! Una vez que incluyamos todo el código de Cypress deberíamos obtener el siguiente resultado.

Todas las pruebas pasando exitosamente.

Ahora podemos corroborar que estas pruebas están funcionando cambiando por ejemplo el título de la página en el archivo src/index.html . Si cambiamos por ejemplo a lo siguiente:

./src/index.html

import { Component } from '@angular/core';@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'medium';
}

Presionamos el botón para recargar las pruebas (marcado en un cuadro rojo en la próxima imagen) y veremos que la especificación de nuestro software ha sido rota y las pruebas nos alertaron de ello.

Conclusiones

Si bien este es un ejemplo super básico en el que sólo corroboramos atributos y texto presentes en la página, esto nos sirve como base para conocer este flujo de trabajo que nos permitirá trabajar sobre un software legado sin el miedo de modificar código ya existente ya que ante cualquier cambio que rompa alguna funcionalidad de la aplicación seremos alertados por las pruebas.

También cabe mencionar que cuando la cantidad de pasos automatizados que introducimos crece demasiado es bueno sentarse a pensar que pasos podrían ser re-utilizables y debemos seleccionar la mejor frase que englobe esas especificaciones tal y como lo hicimos en el caso del paso “existe un texto tipo lista con el texto {string} y el atributo href {string}” en nuestro ejemplo.

Siempre será recomendable indagar en la documentación de Cypress y de Cypress Cucumber preprocessor para ejemplos más avanzados investigando también sobre que otras cosas pueden definirse con Gherkin como Scenario Outline y otras formas de escribir pruebas más especializadas.

Si se genera un ambiente de trabajo expedito entre la definición de los requerimientos utilizando estas herramientas, se puede lograr trabajar con el área de negocios de manera de especificar pasos y condiciones que se deben cumplir y con nuestros archivos .feature podemos generar una suerte de “Contrato Digital” con pruebas automatizadas que se aseguran que el software tiene la calidad esperada.

Y para quienes utilicen Visual Code como editor de código recomiendo instalar la extensión Cucumber (Gherkin) Full Support.

Espero que les haya sido de utilidad y cualquier duda/sugerencia/crítica me lo hacen saber en los comentarios.

--

--

Gonzalo Pincheira A.
Angular Chile

Web development enthusiast — Software Engineer — Musician- Software Designer Developer at Globant