BDD en JavaScript con CucumberJS

0

Por Todd Anderson

He escrito anteriormente sobre desarrollo impulsado por pruebas (TDD) en JavaScript, sobre todo utilizando la biblioteca de estilo de desarrollo impulsado por el comportamiento (BDD) Jasmine en una serie sobre la construcción de una Aplicación de Lista de Compras Impulsada por pruebas. En esa serie de publicaciones, pasé por pensar en Historias de Usuarios para Funciones y Escenarios como tareas de desarrollo reales, y, al leer de nuevo, todo es muy verde (sin juego de palabras) en la medida en que estoy encontrando una manera de entregar código impulsado por pruebas. No hay nada de malo en eso y lo más probable es que vea este y los mensajes posteriores de la misma manera. Dicho esto, sigo siendo cierto que TDD es la mejor manera de entregar un código conciso, probado y bien pensado.

Desde entonces, sin embargo, he incorporado una herramienta diferente en mi flujo de trabajo de TDD para proyectos basados en JavaScript que me permite la integración de especificaciones de características más estrechamente con mi desarrollo y realmente abarca mi ideal actual de Desarrollo Impulsado por el Comportamiento: CucumberJS. Esencialmente, me permite adherirme verdaderamente al TDD mientras desarrollo desde el exterior pruebas automatizadas en ejecución que fallan hasta que he escrito código que admite una función.

Suposiciones y notas

Para los ejemplos de esta publicación, se supone que está familiarizado con NodeJS, npm, desarrollo de módulos de nodos y prácticas comunes de pruebas unitarias, ya que estos temas son demasiado grandes para discutirlos en esta publicación.

Los archivos de soporte relacionados con este tema estarán disponibles en:
https://github.com/bustardcelly/cucumberjs-examples

CucumberJS

CucumberJS es un puerto JavaScript de la popular herramienta BDD Cucumber (que a su vez fue una reescritura de RSpec). Le permite definir Especificaciones de características en un Lenguaje Específico de Dominio (DSL), llamado Gherkin, y ejecutar sus especificaciones utilizando una herramienta de línea de comandos que reportará el paso y/o fallo de los escenarios y los pasos que los componen.

Es importante tener en cuenta que el pepino en sí no proporciona una biblioteca de aserciones predeterminada. Es un marco de pruebas que proporciona una herramienta de línea de comandos que consume Características definidas y valida escenarios ejecutando Pasos escritos en JavaScript. Es la opción de los desarrolladores incluir la biblioteca de aserciones deseada utilizada para que esos pasos pasen o fallen. Es mi intención aclarar el proceso por ejemplo a través de una sola Función con múltiples Escenarios en este post.

Instalación

Puede instalar CucumberJS en su proyecto utilizando npm:

$ npm install cucumber --save-dev

Pepinillo

Si lo había seguido en la serie TDD anterior, encontrará las especificaciones definidas en esa serie similares a Pepinillo. De hecho, voy a volver a hashear una especificación de características de esa serie para demostrar el trabajo a través de su primer cuke (también conocido como, pasar la especificación de características).

Si fuéramos a rehacer la aplicación de la lista de compras bajo TDD / BDD usando Pepino, primero comenzaríamos con una función que usa la sintaxis de Pepinillo:

/features/add-item.característica

Feature: Shopper can add an item to their Grocery List As a grocery shopper I want to add an item to my grocery list So that I can remember to buy that item at the grocery store Scenario: Item added to grocery list Given I have an empty grocery list When I add an item to the list Then The grocery list contains a single item Scenario: Item accessible from grocery list Given I have an empty grocery list When I add an item to the list Then I can access that item from the grocery list

La característica define un valor de negocio, mientras que los escenarios definen los pasos que proporcionan ese valor. Muy a menudo, en el mundo del desarrollo de software, es a partir de estos escenarios que se asumen las tareas de desarrollo y se definen las pruebas de control de calidad.

Me detuve en dos escenarios, pero podríamos agregar fácilmente más escenarios a esta característica; inmediatamente me vienen a la mente reglas de inserción de elementos y validación de propiedades que permiten agregar o rechazar un elemento. En retrospectiva, podría tener más sentido crear especificaciones de características separadas para tales detalles. Sin embargo, podríamos dedicar una publicación completa a estos temas, así que volvamos a la función ya definida.

Dentro de cada Escenario hay una lista de Pasos secuenciales: Dados, Cuándo y Después. Son estos pasos los que CucumberJS ejecutará después de haber consumido esta especificación de características. Después de cada uno de ellos, puede opcionalmente tener Y y Pero, sin embargo, aunque necesario e inevitable a veces, trato de mantenerme alejado de tales cláusulas de paso adicionales.

Ejecutándolo

Habiéndolo guardado en un archivo en un directorio /features, podemos ejecutarlo en Pepino:

$ node_modules/.bin/cucumber-js

De forma predeterminada, CucumberJS consumirá todas las especificaciones de características que se encuentren en el directorio relativo /features.

La salida de la consola actual se verá como la siguiente, lo que esencialmente significa que no se han localizado ni definido todos los pasos:

UUUUUU2 scenarios (2 undefined)6 steps (6 undefined)You can implement step definitions for undefined steps with these snippets:this.Given(/^I have an empty grocery list$/, function(callback) { // express the regexp above with the code you wish you had callback.pending();});this.When(/^I add an item to the list$/, function(callback) { // express the regexp above with the code you wish you had callback.pending();});this.Then(/^The grocery list contains a single item$/, function(callback) { // express the regexp above with the code you wish you had callback.pending();});this.Then(/^I can access that item from the grocery list$/, function(callback) { // express the regexp above with the code you wish you had callback.pending();});

Así que tenemos 6 Pasos indefinidos que conforman 2 Escenarios y la herramienta CucumberJS ci incluso proporciona ejemplos de cómo definirlos.

Una parte importante de ese fragmento de código a entender es que solo hay 4 pasos para implementar. En nuestra Función tenemos 2 Escenarios cada uno con 3 Pasos. Hay un total de 6 pasos, pero solo necesitamos definir 4. La razón es que cada Escenario comparte el mismo paso Dado y Cuándo; estos solo deben definirse una vez y se ejecutarán por separado para cada escenario. Esencialmente, si define Pasos similares utilizando el mismo contexto, reutilizará la «configuración» para un solo Paso dentro de cada escenario.

Uso «configuración» entre comillas porque lo digo más en el papel de definir el contexto para los pasos de Cuándo y Después.

No quiero confundirlo con los métodos de configuración/desmontaje de otras prácticas de pruebas unitarias, que se conocen como tareas de soporte Antes/Después en CucumberJS, y llevar más contexto para configurar un entorno en el que se ejecutan las pruebas (como llenar una base de datos de usuarios) y luego eliminar esa configuración.

Definiciones de pasos

En la sección anterior, vimos que ejecutar CucumberJS contra nuestra Función Agregar elemento nos alertó de que tenemos escenarios indefinidos (y, aunque no impresos en rojo, fallidos) para admitir la función. De forma predeterminada, CucumberJS lee en todas las entidades del directorio /features en relación con el lugar donde se ejecutó el comando, pero no pudo localizar los archivos de pasos compatibles en los que se definieron estos métodos.

Como se mencionó anteriormente, CucumberJS no proporciona una biblioteca de aserciones. La única suposición en este punto, ya que la herramienta CucumberJS se ejecuta bajo NodeJS, es que los pasos admitidos se cargarán como módulos de nodo con una función exportada que se ejecutará. A medida que comencemos a implementar los pasos, necesitaremos decidir la biblioteca de aserciones que usaremos para validar nuestra lógica. Pondremos esa decisión en el estante en este momento y haremos que la configuración de barebones falle.

Para comenzar, tomemos las definiciones de pasos proporcionadas por la herramienta CucumberJS ci y colóquelas en un módulo de nodo:

/ features/stepdefinitions / add-item.Steps.js_

'use strict';module.exports = function() { this.Given(/^I have an empty grocery list$/, function(callback) { callback.pending(); }); this.When(/^I add an item to the list$/, function(callback) { callback.pending(); }); this.Then(/^The grocery list contains a single item$/, function(callback) { callback.pending(); }); this.Then(/^I can access that item from the grocery list$/, function(callback) { callback.pending(); });};

De forma predeterminada, CucumberJS buscará los pasos que se cargarán dentro de una carpeta titulada step_definitions en el directorio / features en relación con el lugar donde emita el comando. Opcionalmente, puede usar la opción -r para que CucumberJS cargue pasos desde otra ubicación. Ejecutar el valor predeterminado es lo mismo que configurar la opción directorio de definición de paso siguiente:

./node_modules/.bin/cucumber-js -r features/step_definitions

La salida de la consola ahora se verá como la siguiente:

P--P--2 scenarios (2 pending)6 steps (2 pending, 4 skipped)

No es demasiado sorprendente ya que notificamos la devolución de llamada de un estado pending. CucumberJS entra en el primer paso (Dado) y se devuelve inmediatamente con una notificación pendiente. Como tal, no se molesta en ingresar ningún paso posterior y los marca como omitidos.

Nota: Es demasiado entrar en una discusión sobre los módulos del lado del cliente y AMD vs CommonJS. Para los propósitos de este ejemplo, usaré CommonJS, ya que mis intereses actuales residen en utilizar Browserify para el desarrollo del lado del cliente. Durante mucho tiempo, fui un defensor de RequireJS y AMD. Una vez más, esa es otra discusión.

Dado

Para acercarnos al verde, abordaremos primero el paso dado:

/características/definiciones de pasos / add-item.paso.js_

'use strict';var GroceryList = require(process.cwd() + '/script/model/grocery-list');module.exports = function() { var myList; this.Given(/^I have an empty grocery list$/, function(callback) { myList = GroceryList.create(); callback(); }); ...};

Si lo ejecutáramos de nuevo, obtendríamos una excepción de inmediato:

$ ./node_modules/.bin/cucumber-jsmodule.js:340 throw err; ^Error: Cannot find module './script/model/grocery-list' at Function.Module._resolveFilename (module.js:338:15) at Function.Module._load (module.js:280:25) at Module.require (module.js:364:17) at require (module.js:380:17) at Object.<anonymous> (/Users/toddanderson/Documents/workspace/custardbelly/cucumberjs-example/features/step_definitions/add-item.steps.js:3:19)

Lo cual es comprensible, ya que no hemos definido ningún otro código excepto este módulo de definición de pasos y estamos tratando de requerir un módulo que no existe. Al seguir con TDD, esto es algo bueno, sabemos por qué está fallando y lo esperamos, ¡me arrancaría el cabello si no lanzara una excepción!

Para que esto pase, crearemos un módulo de nodo en el directorio especificado que exporta un objeto con un método create:

/ script / model / grocery-list.js

'use strict';module.exports = { create: function() { return Object.create(null); }};

Hemos proporcionado el requisito mínimo para que nuestro paso dado pase. Nos preocuparemos por los detalles a medida que nos acerquemos a los últimos pasos.

Ejecute eso de nuevo, y CucumberJS ingresa al paso Cuándo de cada escenario y aborta debido a un retorno pendiente.

$ ./node_modules/.bin/cucumber-js.P-.P-2 scenarios (2 pending)6 steps (2 pending, 2 skipped, 2 passed)

Cuando

En la sección anterior, para hacer pasar el paso Dado en cada Escenario implementamos los inicios de un modelo de Lista de comestibles generado a partir de un método de fábrica, create, del módulo grocery-list. No quiero entrar en un debate sobre la creación de objetos, el operador new, clases y prototipos, al menos no en este post, y asumiré que está familiarizado y cómodo (al menos en la lectura de código) con el Objeto.crear definido para ECMAScript 5.

En la revisión del paso Cuándo para los escenarios:

When I add an item to the list

…necesitamos proporcionar una forma de agregar un elemento a la instancia de Lista de compras creada en el Dado, y hacerlo en un pequeño código para que el paso pase.

En primer lugar, definiremos nuestra expectativa de maquillaje y add firma de la Lista de compras en las definiciones de pasos:

/características/definiciones de pasos/add-item.paso.js_

...module.exports = function() { var myList, listItem = 'apple'; this.Given(/^I have an empty grocery list$/, function(callback) { myList = GroceryList.create(); callback(); }); this.When(/^I add an item to the list$/, function(callback) { myList.add(listItem); callback(); }); ...};

Si ejecutamos de nuevo:

$ ./node_modules/.bin/cucumber-js.F-.F-(::) failed steps (::)TypeError: Object object has no method 'add'

Oooo-weee! Ahora estamos hablando. F grandes y rojas brillantes. 🙂

Para volver a pasar, modificaremos grocery-list con el menor código posible:

/ guión / modelo / lista de comestibles.js

'use strict';var groceryList = { add: function(item) { // }};module.exports = { create: function() { return Object.create(groceryList); }};

Se ejecuta de nuevo, y CucumberJS ha progresado a los pasos de Entonces que informan un estado pending.

$ ./node_modules/.bin/cucumber-js..P..P2 scenarios (2 pending)6 steps (2 pending, 4 passed)

Luego

Avanzamos a través de nuestras implementaciones de pasos y hemos alcanzado los pasos en los que afirmamos operaciones y propiedades que prueban que nuestro escenario proporciona el valor deseado. Como se mencionó anteriormente, CucumberJS no proporciona una biblioteca de aserciones. Mi preferencia en las bibliotecas de aserciones es una combinación de Chai, Sinon y Sinon-Chai, pero para los ejemplos de este post, solo voy a usar el módulo assert que viene con NodeJS. Te animo a que eches un vistazo a otras bibliotecas de afirmaciones y dejes una nota si tienes una favorita.

Nota: Esta sección será un poco pesada, ya que cambiamos rápidamente de modificar nuestro código y ejecutamos el corredor de especificaciones con frecuencia.

Primer escenario

Al revisar el paso del primer escenario:

Then The grocery list contains a single item

…necesitaremos demostrar que la instancia de la Lista de compras crece en un factor de 1 por cada nuevo elemento agregado.

Actualice el paso para definir cómo esperamos que se valide esa especificación:

/ feature / stepdefinitions / add-item.paso.js_

...var assert = require('assert');...module.exports = function() {... this.Then(/^The grocery list contains a single item$/, function(callback) { assert.equal(myList.getAll().length, 1, 'Grocery List should grow by one item.'); callback(); });...};...

Hemos incorporado el módulo assert e intentamos validar que la longitud de la Lista de la compra haya crecido en un valor de 1 después de haber ejecutado el paso anterior, Al agregar el artículo.

Ejecute eso y obtendremos una excepción:

$ ./node_modules/.bin/cucumber-js..F..P(::) failed steps (::)TypeError: Object #<Object> has no method 'getAll'

Agreguemos ese método a nuestro modelo de Lista de compras:

/ guión / modelo / lista de comestibles.js

'use strict';var groceryList = { add: function(item) { // }, getAll: function() { // }};module.exports = { create: function() { return Object.create(groceryList); }};

Y volver a ejecutar nuestras especificaciones:

$ ./node_modules/.bin/cucumber-js..F..P(::) failed steps (::)TypeError: Cannot read property 'length' of undefined

Dado que el código no devuelve nada de getAll(), no podemos acceder a una propiedad length para nuestra prueba de aserción.

Si modificamos el código para devolver un Array:

/ feature/stepdefinitions / add-item.paso.js_

...getAll: function() { return ;}...

Y ejecutar las especificaciones de nuevo, obtendremos el mensaje de error de aserción que proporcionamos:

$ ./node_modules/.bin/cucumber-js..F..P(::) failed steps (::)AssertionError: Grocery List should grow by one item.

Ahora, tenemos un fallo apropiado que se nos informa de una afirmación que hace que el paso no pase. ¡Hurra!

Tome un respiro

Hagamos una pausa aquí por un segundo antes de agregar más código para pasar este paso. El problema en cuestión no es en realidad agregar un elemento a la matriz que se devuelve, se trata más de garantizar que un elemento se agregue a través del método add y que el resultado de getAll sea una lista extendida con ese elemento.

Los detalles de implementación que participan en la aprobación de esta prueba son donde su equipo utiliza su experiencia de arquitectura, pero se requiere cuidado de que solo se agregue el código más esencial. Debe evitar exagerar al pensar en los aspectos internos del modelo de recolección de la Lista de compras. Es una cuerda apretada resbaladiza que podría caer fácilmente por una madriguera de conejo, al igual que esa metáfora mal redactada 🙂

¡Vuelve al trabajo!

Para los fines de este ejemplo, usaremos el argumento propertiesObject de Object.create para definir un getter list que servirá como una matriz mutable para nuestros artículos de la lista de comestibles:

/script/model/grocery-list.js

'use strict';var groceryList = { add: function(item) { this.list.push(item); }, getAll: function() { return this.list; }};module.exports = { create: function() { return Object.create(groceryList, { 'list': { value: , writable: false, enumerable: true } }); }};

Si ejecutamos eso, ¡encontraremos que el primer escenario está pasando!

$ ./node_modules/.bin/cucumber-js.....P2 scenarios (1 pending, 1 passed)6 steps (1 pending, 5 passed)

Segundo escenario

Al revisar el paso final de nuestro segundo escenario, la implementación pendiente está accediendo al elemento agregado:

Then I can access that item from the grocery list

Para pasar este paso, necesitamos verificar que podemos acceder al artículo adjunto a la Lista de compras invocando add() con un artículo.

Al igual que con la implementación de acceder a la longitud de la Lista de compras, hay varias formas en las que podríamos aprobar esta prueba en el código. De nuevo, siento que aquí es donde la experiencia y el gusto en el desarrollo de software entran en juego con respecto a la arquitectura, pero también prefiero tratar de producir la menor cantidad de código posible; y seré el primero en admitir que a veces voy un poco distraído y creo más código del necesario hence por lo tanto, lo intento.

Dicho esto, también tenemos que tener en cuenta las especificaciones de lenguaje y entorno en la forma en que abordamos la transmisión de la afirmación, y el navegador, con su historial, tiene muchos a considerar. Eso no es un desaire, es solo una previsión al establecer expectativas para los requisitos.

Específicamente: supongamos que decimos que el paso se puede verificar utilizando el método Array.indexOf() en la colección devuelta desde ‘getAll()’ en el objeto de la Lista de compras? Sin un relleno de polietileno, entonces nos estamos limitando a pasar afirmaciones en IE 9 y mayores. Estas consideraciones son solo la punta del iceberg a la hora de decidir qué introducir en su base de código para que sus pruebas pasen, y realmente deben dejarse a una discusión en equipo sobre lo que se considera necesario para llevar el producto a producción.

Podría seguir y seguir, pero asumamos que queremos cubrir todas las bases cuando se trata de navegadores (es decir, 6 y más, estremecimiento). En mi opinión, para que este segundo escenario se vuelva verde, agregaremos un método getItemIndex() con la siguiente firma:

+ getItemIndex(itemValue):int

Primero modificaremos el paso para fallar:

/ características / definiciones de pasos/complemento.paso.js_

this.Then(/^I can access that item from the grocery list$/, function(callback) { assert.notEqual(myList.getItemIndex(listItem), -1, 'Added item should be found at non-negative index.'); callback();});

La aceptación para que esta prueba pase es que el índice en el que reside el elemento agregado en la colección no sea negativo. Para este escenario, no estamos tratando de validar una especificación sobre dónde se agrega un nuevo elemento en una lista (por ejemplo, antepuesto o agregado), sino simplemente que es accesible.

En ejecución que producirá una excepción:

$ ./node_modules/.bin/cucumber-js.....F(::) failed steps (::)TypeError: Object #<Object> has no method 'getItemIndex'

Modifiquemos nuestro objeto de lista de compras para admitir el método getItemIndex:

/ script / model / grocery-list.js

'use strict';var groceryList = { add: function(item) { this.list.push(item); }, getAll: function() { return this.list; }, getItemIndex: function(value) { var index = this.list.length; while(--index > -1) { if(this.list === value) { return index; } } return -1; }};module.exports = { create: function() { return Object.create(groceryList, { 'list': { value: , writable: false, enumerable: true } }); }};

En nuestra implementación de getItemIndex, la lista se recorre y, si se encuentra el elemento, se devuelve el índice. De lo contrario, se devuelve un valor de -1. Esencialmente, cómo funciona el método Array.indexOf de ECMAScript 5.

Nota: Sé que puede parecer tonto usar Objetos.crear a partir de ECMAScript 5, pero no de matriz.indexOf. La razón, principalmente, es que normalmente siempre incluyo un relleno múltiple para Objeto.crear y no para matriz.indexOf. Supongo que es un hábito.

Ahora si volvemos a ejecutar las especificaciones bajo CucumberJS:

$ ./node_modules/.bin/cucumber-js......2 scenarios (2 passed)6 steps (6 passed)

¡Nuestros cukes son VERDES! (Este es el punto en el que se limpia la frente y aplaude lentamente).

Conclusión

En este post, presenté cómo uso la herramienta BDD CucumberJS para adherirme al Desarrollo Impulsado por pruebas en JavaScript. Usé un ejemplo de una sola función con dos escenarios y convertí los pasos fallidos en cukes verdes. Si no está familiarizado con el proceso de hacer que las pruebas fallen primero solo para producir código para que la prueba pase, espero que lo haya seguido; Puedo ser prolijo y el proceso podría parecer que toma mucho tiempo, pero el desarrollo bajo tales prácticas comienza a moverse sin problemas una vez que entras en el ritmo. Además, creo que hay una gran recompensa en tener su código bajo un arnés de prueba cuando se trata de refactorización y corrección de errores, tanto en la salud del desarrollador como en los negocios.

Este artículo fue publicado originalmente en http://custardbelly.com/blog/blog-posts/2014/01/08/bdd-in-js-cucumberjs/index.html

Imagen cortesía de http://commons.wikimedia.org/wiki/File:Og%C3%B3rki…jpg

e6b2cff6a55928c8f62b9d602add3fed?s = 100d = mmr = g

Todd Anderson es un desarrollador de aplicaciones apasionado por la arquitectura y los flujos de trabajo de desarrollo.

Como un fuerte defensor de las prácticas ágiles y el Desarrollo Basado en pruebas con más de una década de experiencia, ha ayudado a entregar soluciones web, móviles y de escritorio con numerosas empresas en los sectores empresarial y de entretenimiento, incluidas Adobe, THQ, Condé Nast Publications y Motorola.

Escribe con frecuencia sobre software y tecnología en su blog, apoya el software de código abierto y debido a mi mejor esfuerzo para retribuir a la comunidad de desarrollo a través de GitHub y ha tenido el estimado placer de ser coautor de varios títulos de O’Reilly y Wiley Wrox: Perfil de Amazon.

Siga a Todd en Twitter

Visite el sitio de Todd

Deja una respuesta

Tu dirección de correo electrónico no será publicada.