loading...
Cover image for BDD automated-testing con Gherkin y Jest en Node.js 🦄

BDD automated-testing con Gherkin y Jest en Node.js 🦄

imsergiobernal profile image Sergio ・8 min read

Creo que los test en una aplicación son la mejor documentación que podemos crear. No solo son pruebas, también pueden narrar el comportamiento de la aplicación y sus restricciones.

Además, es un buen punto donde las necesidades de negocio y el QA se pueden acercar al nivel de desarrollo.

Pero gestionar el testing de una aplicación no es tarea fácil, porque las necesidades se transforman en el tiempo y se nos requiere mantener toda la estructura de tests que tenemos desarrollada. Por si fuera poco, también tenemos que mantener la documentación, arquitectura de la aplicación, etc.

Es por eso que la automatización se vuelve un enclave vital para mantener nuestro proyecto lo más ágil (sí, agile en Español) posible.

BDD TDD

Aquí es donde vamos a entrar en juego.

Caso en un entorno Agile

Todo comienza por una necesidad.

Vamos a suponer que formamos parte de un equipo de 3 desarrolladores emprendedores los cuales nos hemos repartido roles en el de desarrollo de software. Nosotros (usted lector y yo) nos vamos a encargar de las soluciones más abstractas del proyecto.

Uno de los compañeros quiere que desarrollemos la implementación de una estructura de datos de una Lista Enlazada. Él no se quiere encargar de dicha abstracción, por que está dedicado su tiempo a concreciones de más alto nivel para el negocio. Sin embargo, esta necesidad es importante porque un error en la implementación podría suponer un gran coste y un sin fin de problemas. Además pensamos utilizarla en varias partes del proyecto.

Realmente él habría hecho el mismo proceso de Descubrimiento, Formulación, Automatización que vamos a narrar en este artículo pero con un lenguaje más de producto y no tanto de ciencias de la computación.

Para el que no lo sepa, una Lista Enlazada tiene la siguiente estructura. Aquí podrás encontrar más información.

Lista Enlazada

Forma visual de una Lista Enlazada

Quien se dedique a implementar algoritmos y estructuras de datos sabrá que es fácil colarse por algún lado. Es importante sentarse previamente a definir cómo funcionará, aunque normalmente todos vamos corriendo al código 😌. En este caso, la Lista Enlazada ya está más que definida porque es muy popular y muy sencilla.

Practicamos un poco de BDD y nos sentamos con nuestro compañero emprendedor para desarrollar la historia de usuario.

Descubrimiento: lo que podría hacer

Nos pide que necesita la implementación de la Lista Enlazada.

Como desarrollador de componentes de la aplicación, quiero crear una lista enlaza con la secuencia de teclas que pulsamos en el teclado para poder implementar la lógica de un historial de comandos.

Historial de comandos de Photoshop

Sería algo parecido al historial de comandos de Photoshop

Conversando con nuestro compañero, descubrimos que para lo que necesita ahora, con la siguiente serie de requerimientos podemos empezar:

Requerimientos

  • Poder instanciar una lista
  • La lista estará vacía por defecto
  • La lista tendrá una longitud
  • La lista ha de tener un HEAD y un TAIL
  • Poder añadir elementos al final de la lista
  • Los elementos guardarán un puntero al siguiente elemento

Formulación: Qué debe de hacer

Sabemos lo que nuestro cliente quiere, ahora vamos a escribir los requerimientos en lenguaje Gherkin para sacar juguito a lo que vamos a ver después 🧃.

Gherkin es un Domain-specific language (DSL) / sintáxis que permite definir muy explícitamente el comportamiento de una Feature y los criterios de aceptación. Se puede utilizar para todos los niveles de testing sin restricción, dado que el dominio dará el contexto.

El objetivo de este artículo no es explicar o entrar en detalle sobre Gherkin, pero si tienes algun comentario de mejora o quieres que profundice en más detalle, házlo saber en los comentarios ✍.

Tendremos que hacer un ejercicio por desarrollar los escenarios en base a la información que hemos extraído en el proceso de Descubrimiento previo.

// file 'lib/data-structures/features/singly-linked-list.feature'
Feature: Singly Linked List

  Scenario: Instancing a list
    Given no instance
    When instancing a new Singly Linked List
    Then initial head must be null
    And initial tail must be null
    And length must be 0

  Scenario: Pushing a value on a empty list
    Given an Empty Singly Linked List with 0 nodes
    When pushing 'First Node' as node value
    Then head value becomes 'First Node'
    And tail value becomes 'First Node'
    And the list length becomes 1

  Scenario: Pushing values on a list with a single node
    Given a Singly Linked List with a single node as 'First Node' as value
    When pushing the following values
    | NodeValue   |
    | Second Node |
    | Third Node  |
    Then list length gets incremented to 3
    And tail value becomes 'Third Node'
    But head value still equals to 'First Node'

Opcionalmente le pasamos dicho fichero al compañero para que nos compruebe que hemos entendido correctamente sus necesidades. Si hubiese sido un cliente sin habilidades técnicas también sería capaz de entender este lenguaje, y ese es unos de los principios y beneficios de Gherkin.

Cuando uses Gherkin, describe el comportamiento requerido, no el cómo.

Escribir este tipo de textos no es tarea fácil al principio. No obstante es una disciplina que hay que entrenar y que aumentará la calidad de tus entregas y tu pensamiento como desarrollador. Existen buenas y malas prácticas.

Sufre el dolor de la disciplina o sufre el dolor del arrepentimiento.

Automatización: Qué es lo que realmente hace

Aquí viene lo sexy 😏. El principal coste que tiene trabajar con Gherkin es que hay que mantener dos cosas: la definición de la feature y los tests.

Pero amigo mio, vamos a hacer que mantener las dos cosas sea muy liviano, porque definiones y tests estarán enlazados explícitamente a través de jest + jest-cucumber.

Jest 💗 Cucumber

Instalemos ambos paquetes

npm i -D jest jest-cucumber

La conexión entre los tests y los ficheros de definición de .feature es bidireccional y muy práctica.

Cuando hagamos un mínimo cambio en el fichero singly-linked-list.feature, el fichero singly-linked-list.steps.js fallará 🚨 para avisarnos de que hay adaptaciones por hacer. Sólo así lograremos lo que se denomina una Documentacion viva real.

Hay que entender que si el fichero singly-linked-list.feature cambia, es porque han habido cambios de negocio. De haber habido cambios en el negocio, nunca estará de más que queden explícitos. Esto lo hacemos a través de dicho fichero. Esto permitirá que el software escale mucho más, sobretodo con la incorporación de nuevas personas al equipo.

Si recién te incorporas a un proyecto, te darás cuenta que hacer TDD puede ser casi imposible. Con la ayuda semántica de los ficheros .feature a través de BDD este proceso se hace mucho más armonioso.

Por otra parte, si el título de alguno de los tests no es exacto a la definición, también fallará. Vamos a poner esto a prueba.

Vamos a crear el siguiente steps test incompleto y vamos a cargar la .feature a través de la función loadFeature():

// file 'lib/data-structures/features/steps/singly-linked-list.steps.js'
const { loadFeature, defineFeature } = require('jest-cucumber');
const { SinglyLinkedList } = require('./SinglyLinkedList');

const feature = loadFeature('./SinglyLinkedList.feature', { loadRelativePath: true, errors: true });

defineFeature(feature, test => {
    test('Instancing a list', ({ given, when, then, and }) => {

        let list;

        given('no instance', () => {
            expect(list).toBeUndefined;
        });

        when('instancing a new Singly Linked List', () => {
            list = new SinglyLinkedList();
        });

        then('initial head must be null', () => {
            expect(list.head).toBe(null);
        });
        and('initial tail must be null', () => {
            expect(list.tail).toBe(null);
        });
        and('length must be 0', () => {
            expect(list.length).toBe(0);
        });
    });

    test('Pushing a value on a empty list', ({ given, when, then, and }) => {

        let list;

        given(/^an Empty Singly Linked List with (.*) nodes$/, (arg0) => {
            list = new SinglyLinkedList();

            expect(list.length).toBe(Number.parseInt(arg0));
        });

        when(/^pushing (.*) as node value$/, (arg0) => {
            list.push(arg0);
        });

        then(/^head value becomes (.*)$/, (arg0) => {
            expect(list.head.value).toBe(arg0);
        });
        and(/^tail value becomes (.*)$/, (arg0) => {
            expect(list.tail.value).toBe(arg0);
        });
        and(/^the list length becomes (.*)$/, (arg0) => {
            expect(list.length).toBe(Number.parseInt(arg0));
        });
    });
});

Ahora corremos Jest para que compruebe los test y obtendríamos el siguiente resultado:

$ npx jest
 PASS  19. Singled Linked List/SinglyLinkedList.steps.js  Singly Linked List
    √ Instancing a list (5ms)
    √ Pushing a value on a empty list (1ms)
    √ Pushing values on a list with a single node (1ms)

 FAIL  19. Singled Linked List/SinglyLinkedList-demo.steps.js
  Singly Linked List
    × encountered a declaration exception (9ms)

  ● Singly Linked List › encountered a declaration exception

Feature file has a scenario titled "Pushing values on a list with a single node", but no match found in step definitions. Try adding the following code:

test('Pushing values on a list with a single node', ({ given, when, then, and, but }) => {
  given('a Singly Linked List with a single node as \'First Node\' as value', () => {

   });

    when('pushing the following values', (table) => {

    });

    then(/^list length gets incremented to (.*)$/, (arg0) => {

    });

    and('tail value becomes \'Third Node\'', () => {

    });

    but('head value still equals to \'First Node\'', () => {

    });
});

Como has podido observar, falta el Scenario: Pushing values on a list with a single node y Jest nos está regalando un precioso copy-paste con el que podremos salir del apuro y ahorrar tiempo. Es una plantilla que se puede mejorar si así lo deseamos; vamos a desarrollar la implementación y hacer que los Third Node y First Node se pasen como argumentos para que el test sea más explícito. El fichero final quedará así:

// file 'lib/data-structures/features/steps/singly-linked-list.steps.js'
    ...
    test('Pushing values on a list with a single node', ({ given, when, then, and, but }) => {

        let list;

        given(/^a Singly Linked List with a single node as '(.*)' as value$/, (arg0) => {
            list = new SinglyLinkedList();
            list.push(arg0);

            expect(list.length).toBe(1);
        });

        when('pushing the following values', (table) => {
            table.forEach((row) => {
                list.push(row.NodeValue);
            });
        });

        then(/^list length gets incremented to (.*)$/, (arg0) => {
            expect(list.length).toBe(Number.parseInt(arg0));
        });
        // Utilizo Regex para determinar qué valor será pasado a través de arg0
        and(/^tail value becomes '(.*)'$/, (arg0) => {
            expect(list.tail.value).toBe(arg0);
        });
        // Utilizo Regex para determinar qué valor será pasado a través de arg0
        but(/^head value still equals to '(.*)'$/, (arg0) => {
            expect(list.head.value).toBe(arg0);
        });
    });
});

Y ahora sí, los tests serán 💚.

Bonus: Extensiones de VSCode

Voy a mencionar dos extensiones que pueden llevar esto a otro nivel de productividad

Jest
VSCode estará corriendo los tests de fondo y permitirá ver en tiempo real el resultado de ellos, así como debuggear de manera individual.

Jest demo

Cucumber (Gherkin) Full Support
Syntax highligther, gherkin-to-test copy paste converter, autocompletion ... Una maravilla.

Cucumber demo

¿Qué te ha parecido el tema tratado? ¿Te puedo ayudar en algo 🐱‍👓? Muchas gracias por dedicar tu tiempo a leer este artículo y nos vemos en el siguiente.

Discussion

markdown guide