BDD en JavaScript avec CucumberJS

0

Par Todd Anderson

J’ai déjà écrit sur le développement piloté par les tests (TDD) en JavaScript, notamment en utilisant la bibliothèque de style behavior driven development (BDD) Jasmine dans une série sur la construction d’une application de liste de courses pilotée par les tests. Dans cette série de messages, j’ai pensé aux Histoires d’utilisateurs pour les fonctionnalités et les scénarios comme des tâches de développement réelles, et – en y lisant – tout est très vert (sans jeu de mots) dans la mesure où je trouve un moyen de fournir du code piloté par les tests. Il n’y a rien de mal à cela et je vais très probablement examiner cela et les messages suivants de la même manière. Cela dit, je reste vrai que TDD est le meilleur moyen de fournir un code concis, testé et bien pensé.

Depuis ce temps, cependant, j’ai intégré un outil différent dans mon flux de travail TDD pour les projets basés sur JavaScript qui me permet d’intégrer plus étroitement les spécifications des fonctionnalités à mon développement et englobe vraiment mon idéal actuel de Développement axé sur le comportement: CucumberJS. Essentiellement, cela me permet de vraiment adhérer à TDD tout en développant de l’extérieur en exécutant des tests automatisés qui échouent jusqu’à ce que j’aie écrit du code prenant en charge une fonctionnalité.

Hypothèses et notes

Pour les exemples de cet article, il est supposé que vous êtes familier avec les NodeJS, les npm, le développement de modules de nœuds et les pratiques de test unitaires courantes car ces sujets sont trop importants pour être discutés dans cet article.

Les fichiers de support liés à ce sujet seront disponibles à l’adresse suivante::
https://github.com/bustardcelly/cucumberjs-examples

CucumberJS

CucumberJS est un port JavaScript de l’outil BDD populaire Cucumber (qui lui-même était une réécriture de RSpec). Il vous permet de définir des spécifications d’entités dans un langage spécifique au domaine (DSL) – appelé Gherkin – et d’exécuter vos spécifications à l’aide d’un outil de ligne de commande qui rapportera le passage et / ou l’échec des scénarios et les étapes qui les composent.

Il est important de noter que Cucumber lui-même ne fournit pas de bibliothèque d’assertion par défaut. Il s’agit d’un framework de test fournissant un outil de ligne de commande qui consomme des fonctionnalités définies et valide des scénarios en exécutant des étapes écrites en JavaScript. C’est le choix des développeurs d’inclure la bibliothèque d’assertion souhaitée utilisée afin de faire passer ou échouer ces étapes. J’ai l’intention de clarifier le processus par l’exemple à travers une seule fonctionnalité avec plusieurs scénarios dans cet article.

Installation

Vous pouvez installer CucumberJS dans votre projet en utilisant npm:

$ npm install cucumber --save-dev

Gherkin

Si vous aviez suivi la série TDD précédente, vous trouverez les spécifications définies dans cette série similaires à Gherkin. En fait, je vais re-hacher une spécification de fonctionnalité de cette série pour démontrer le fonctionnement de votre première cuke (c’est-à-dire passer des spécifications de fonctionnalité).

Si nous devions refaire l’application de liste de courses sous TDD/BDD en utilisant le concombre, nous commencerions par une fonctionnalité utilisant la syntaxe du cornichon:

/features/add-item.fonctionnalité

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 fonctionnalité définit une valeur métier, tandis que les Scénarios définissent les étapes qui fournissent cette valeur. Le plus souvent, dans le monde du développement logiciel, c’est à partir de ces scénarios que les tâches de développement sont prises en charge et que les tests d’assurance qualité sont définis.

Je me suis arrêté à deux scénarios, mais nous pourrions très facilement ajouter d’autres scénarios à cette fonctionnalité; ce qui me vient immédiatement à l’esprit, ce sont les règles d’insertion d’éléments et la validation des propriétés qui permettent d’ajouter ou de rejeter un élément. Avec le recul, il pourrait être plus logique de créer des spécifications de fonctionnalités distinctes pour de tels détails. Nous pourrions cependant passer un article entier sur de tels sujets, alors revenons à la fonctionnalité déjà définie.

Dans chaque Scénario se trouve une liste d’étapes séquentielles: Données, Quand et puis. Ce sont ces étapes que CucumberJS exécutera après avoir consommé cette spécification de fonctionnalité. Après chacun de ceux–ci, vous pouvez éventuellement avoir Et et Mais, cependant – bien que nécessaire et inévitable parfois – j’essaie de rester à l’écart de ces clauses d’étape supplémentaires.

L’exécuter

Après l’avoir enregistré dans un fichier dans un répertoire /features, nous pouvons ensuite l’exécuter sous Cucumber:

$ node_modules/.bin/cucumber-js

Par défaut, CucumberJS consommera toutes les spécifications de fonctionnalité trouvées dans le répertoire relatif/features.

La sortie de la console actuelle ressemblera à ce qui suit, ce qui signifie essentiellement que toutes les étapes n’ont pas été localisées ou définies:

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();});

Nous avons donc 6 étapes non définies qui composent 2 scénarios et l’outil CucumberJS ci fournit même des exemples pour les définir!

Une partie importante de cet extrait à comprendre est qu’il n’y a que 4 étapes à implémenter. Dans notre fonction, nous avons 2 scènes chacune avec 3 étapes. Il y a un total de 6 étapes, mais il suffit d’en définir 4. La raison en est que chaque Scénario partage la même étape Donnée et Quand; ceux-ci ne doivent être définis qu’une seule fois et seront exécutés séparément pour chaque Scénario. Essentiellement, si vous définissez des étapes similaires en utilisant le même contexte, il réutilisera la « configuration » pour une seule Étape dans chaque scénario.

J’utilise « setup » entre guillemets parce que je le veux plus dans le rôle de définir le contexte pour les étapes When et Then.

Je ne veux pas le confondre avec les méthodes de configuration / démontage d’autres pratiques de tests unitaires – qui sont connues sous le nom de tâches de support Avant / après dans CucumberJS – et contiennent davantage de contexte pour la configuration d’un environnement dans lequel les tests sont ensuite exécutés (comme remplir une base de données d’utilisateurs), puis démolir cette configuration.

Définitions d’étape

Dans la section précédente, nous avons vu que l’exécution de CucumberJS contre notre fonctionnalité d’ajout d’élément nous alertait que nous avions des scénarios non définis (et, bien que non imprimés en rouge, défaillants) pour prendre en charge la fonctionnalité. Par défaut, CucumberJS lit toutes les entités du répertoire /features par rapport à l’endroit où la commande a été exécutée, mais il n’a pas pu localiser les fichiers d’étapes pris en charge dans lesquels ces méthodes sont définies.

Comme mentionné précédemment, CucumberJS ne fournit pas de bibliothèque d’assertions. La seule hypothèse à ce stade – puisque l’outil CucumberJS est exécuté sous NodeJS – est que les étapes prises en charge seront chargées en tant que modules de nœud avec une fonction exportée à exécuter. Au fur et à mesure que nous commençons à implémenter les étapes, nous devrons décider de la bibliothèque d’assertions à utiliser pour valider notre logique. Nous allons mettre cette décision sur l’étagère pour le moment et faire échouer la configuration des barebones.

Pour commencer, prenons les définitions d’étape fournies par l’outil CucumberJS ci et déposons-les dans un module de nœud:

/features/stepdefinitions/add-item.étape.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(); });};

Par défaut, CucumberJS recherchera les étapes à charger dans un dossier intitulé step_definitions sous le répertoire /features par rapport à l’endroit où vous émettez la commande. Vous pouvez éventuellement utiliser l’option -r pour que CucumberJS charge les étapes à partir d’un autre emplacement. L’exécution de la valeur par défaut revient à définir l’option de répertoire de définition d’étape suivante:

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

La sortie de la console ressemblera maintenant à ce qui suit:

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

Pas trop surprenant vu que nous notifions le rappel d’un état pending. CucumberJS entre dans la première étape (Donnée) et est immédiatement renvoyé avec une notification en attente. En tant que tel, il ne prend pas la peine d’entrer des étapes ultérieures et les marque comme ignorées.

Remarque: C’est trop pour entrer dans une discussion sur les modules côté client et AMD vs CommonJS. Aux fins de cet exemple, j’utiliserai CommonJS, car mes intérêts actuels résident dans l’utilisation de Browserify pour le développement côté client. Pendant longtemps, j’ai été un partisan des RequireJS et de la DMLA. Encore une fois, c’est une toute autre discussion.

Donné

Pour nous rapprocher du vert, nous aborderons d’abord l’étape donnée:

/features/stepdefinitions/add-item.étape.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 nous l’exécutions à nouveau, nous obtiendrions immédiatement une exception:

$ ./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)

Ce qui est compréhensible car nous n’avons défini aucun autre code que ce module de définition d’étape et essayons d’exiger un module qui n’existe pas. En restant avec TDD, c’est une bonne chose – nous savons pourquoi il échoue et nous nous y attendons – je me retirerais les cheveux s’il n’y avait pas d’exception!

Pour que cela passe, nous allons créer un module de nœud dans le répertoire spécifié qui exporte un objet avec une méthode create:

/script/model/grocery-list.js

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

Nous avons fourni l’exigence minimale pour que notre étape donnée passe. Nous nous inquiéterons des détails à l’approche des dernières étapes.

Exécutez-le à nouveau, et CucumberJS entre à l’étape When de chaque scénario et abandonne en raison du retour en attente.

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

Lorsque

Dans la section précédente, pour faire passer l’étape Donnée sur chaque Scénario, nous avons implémenté les débuts d’un modèle de Liste d’épicerie généré à partir d’une méthode d’usine, create, du module grocery-list. Je ne veux pas entrer dans un débat sur la création d’objets, l’opérateur new, les classes et les prototypes – du moins pas dans cet article – et supposerai que vous êtes familier et à l’aise (au moins en lisant le code) avec l’objet.créer défini pour ECMAScript 5.

Lors de l’examen de l’étape Quand pour les scénarios:

When I add an item to the list

… nous devons fournir un moyen d’ajouter un élément à l’instance de liste d’épicerie créée dans le Donné – et le faire en aussi peu de code pour faire passer l’étape.

Tout d’abord, nous définirons notre attente du maquillage et de la signature add de la liste d’épicerie dans les définitions d’étape:

/features/stepdefinitions/add-item.étape.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 nous l’exécutons à nouveau:

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

Deee! Maintenant, nous parlons. De gros F rouge vif. 🙂

Pour que cela se reproduise, nous modifierons grocery-list avec le moins de code possible:

/ script / modèle / liste d’épicerie.js

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

s’exécute à nouveau, et CucumberJS a progressé vers les étapes Then qui signalent un état pending.

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

Puis

Nous avons progressé dans nos implémentations par étapes et avons atteint les étapes auxquelles nous affirmons des opérations et des propriétés prouvant que notre scénario fournit la valeur prévue. Comme mentionné précédemment, CucumberJS ne fournit pas de bibliothèque d’assertions. Ma préférence dans les bibliothèques d’assertions est une combinaison de Chai, Sinon et Sinon-Chai, mais pour les exemples de cet article, je vais simplement utiliser le module assert fourni avec NodeJS. Je vous encourage à consulter d’autres bibliothèques d’assertions et à laisser une note si vous avez un favori.

Remarque: Cette section sera un petit exemple lourd car nous passons rapidement de la modification de notre code et exécutons fréquemment le coureur de spécifications.

Premier scénario

Lors de l’examen de l’étape Puis du premier Scénario:

Then The grocery list contains a single item

… nous devrons prouver que l’instance de liste d’épicerie augmente d’un facteur 1 pour chaque nouvel article ajouté.

Mettez à jour l’étape pour définir comment nous nous attendons à ce que cette spécification soit validée :

/feature/stepdefinitions/add-item.étape.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(); });...};...

Nous avons intégré le module assert et tenté de valider que la longueur de la liste d’épicerie a augmenté d’une valeur de 1 après avoir exécuté l’étape précédente – Lors de l’ajout de l’article.

Exécutez cela et nous obtiendrons une exception:

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

Ajoutons cette méthode à notre modèle de liste d’épicerie:

/ script / modèle / liste d’épicerie.js

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

Et revenons à l’exécution de nos spécifications:

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

Étant donné que le code ne renvoie rien de getAll(), nous ne pouvons pas accéder à une propriété length pour notre test d’assertion.

Si nous modifions le code pour retourner un tableau :

/feature/stepdefinitions/add-item.étape.js_

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

Et exécutez à nouveau les spécifications, nous obtiendrons le message d’erreur d’assertion que nous avons fourni:

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

Maintenant, nous avons un échec approprié qui nous est signalé à partir d’une assertion qui fait que l’étape ne passe pas. Hourra!

Faites une pause

Faisons une pause ici pendant une seconde avant d’ajouter plus de code pour que cette étape passe. Le problème actuel n’est pas d’ajouter un élément au tableau renvoyé, il s’agit plutôt de s’assurer qu’un élément est ajouté via la méthode add et que le résultat de getAll est une liste étendue avec cet élément.

Les détails d’implémentation impliqués dans la réussite de ce test sont ceux où votre équipe utilise son expérience en architecture, mais il faut veiller à ce que seul le code le plus essentiel soit ajouté. Vous devriez éviter d’aller trop loin en pensant aux éléments internes du modèle de collection de listes d’épicerie. C’est une corde glissante qui pourrait facilement tomber dans un trou de lapin – tout comme cette métaphore mal formulée 🙂

Retournez au travail!

Pour les besoins de cet exemple, nous utiliserons l’argument propertiesObject de Object.create pour définir un getter list qui servira de tableau mutable pour nos éléments de liste d’épicerie :

/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 nous exécutons cela, nous constaterons que le premier scénario passe maintenant!

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

Deuxième scénario

Lors de l’examen de la dernière étape de notre 2ème scénario, l’implémentation en attente accède à l’élément ajouté:

Then I can access that item from the grocery list

Pour faire passer cette étape, nous devons vérifier que nous pouvons accéder à l’élément ajouté à la liste d’épicerie en appelant add() avec un élément.

Comme pour la mise en œuvre de l’accès à la longueur de la liste d’épicerie, il existe plusieurs façons de faire passer ce test dans le code. Encore une fois, je pense que c’est là que l’expérience et le goût du développement logiciel entrent en jeu en ce qui concerne l’architecture, mais je préfère aussi essayer de produire le moins de code possible; et je serai le premier à admettre que parfois je suis un peu distrait et je crée plus de code que nécessaire… donc, essayer.

Cela dit, nous devons également prendre en compte les spécifications de la langue et de l’environnement dans la façon dont nous traitons la réussite de l’assertion – et le navigateur, avec son historique, a beaucoup à considérer. Ce n’est pas une légère, c’est juste une prévoyance dans l’établissement d’attentes en matière d’exigences.

Plus précisément: supposons que nous devions dire que l’étape peut être vérifiée en utilisant la méthode Array.indexOf() sur la collection renvoyée par ‘getAll()’ sur l’objet de liste d’épicerie? Sans polyfill, nous nous limitons alors à transmettre des assertions sur IE 9 et plus anciens. De telles considérations ne sont que la pointe de l’iceberg au moment de décider quoi introduire dans votre base de code afin de faire passer vos tests, et devraient vraiment être laissées à une discussion d’équipe sur ce qui est jugé nécessaire pour mettre le produit en production.

Je pourrais continuer encore et encore, mais supposons simplement que nous voulons couvrir toutes les bases en ce qui concerne les navigateurs (IE 6 et plus, frissonner). À mon avis, pour que ce deuxième scénario devienne vert, nous ajouterons une méthode getItemIndex() avec la signature suivante:

+ getItemIndex(itemValue):int

Nous allons d’abord modifier l’étape pour échouer:

/feature/stepdefinitions /add-item.étape.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();});

L’acceptation pour que ce test passe est que l’index auquel réside l’élément ajouté dans la collection est non négatif. Pour ce scénario, nous n’essayons pas de valider une spécification quant à l’endroit où un nouvel élément est ajouté dans une liste (par exemple, ajouté ou ajouté), mais simplement qu’il est accessible.

Exécution qui produira une exception:

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

Modifions notre objet Liste d’épicerie pour prendre en charge la méthode 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 } }); }};

Dans notre implémentation de getItemIndex, la liste est parcourue et, si l’élément est trouvé, l’index est renvoyé. Sinon, une valeur de -1 est renvoyée. Essentiellement, comment fonctionne la méthode Array.indexOf d’ECMAScript 5.

Remarque: Je sais qu’il peut sembler idiot d’utiliser un objet.créer à partir d’ECMAScript 5, mais pas de tableau.Index de. La raison – principalement – étant que j’inclus normalement toujours un polyfill pour l’objet.créer et non pour le tableau.Index de. Je suppose que l’habitude.

Maintenant, si nous exécutons à nouveau les spécifications sous CucumberJS:

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

Nos cukes sont VERTS! (C’est le point que vous essuyez votre front et applaudissez lentement).

Conclusion

Dans cet article, j’ai présenté comment j’utilise l’outil BDD CucumberJS afin de respecter le développement piloté par les tests en JavaScript. J’ai utilisé un exemple d’une seule fonctionnalité avec deux scénarios et j’ai transformé les étapes défaillantes en cukes verts. Si vous n’êtes pas familier avec le processus consistant à faire échouer les tests d’abord pour produire du code pour faire passer le test, j’espère que vous avez suivi; Je peux être verbeux et le processus pourrait sembler prendre beaucoup de temps, mais le développement dans le cadre de telles pratiques commence à se dérouler sans problème une fois que vous êtes dans le sillon. De plus, je pense qu’il y a une énorme récompense à avoir votre code sous un harnais de test en matière de refactorisation et de correction de bogues – à la fois dans la santé des développeurs et dans les affaires.

Cet article a été publié à l’origine à http://custardbelly.com/blog/blog-posts/2014/01/08/bdd-in-js-cucumberjs/index.html

Image reproduite avec l’aimable autorisation de http://commons.wikimedia.org/wiki/File:Og%C3%B3rki…jpg

 e6b2cff6a55928c8f62b9d602ajout3fed?s = 100d = mmr = g

Todd Anderson est un développeur d’applications passionné par l’architecture et les flux de travail de développement.En tant que fervent défenseur des pratiques agiles et du développement piloté par les tests avec plus de dix ans d’expérience, il a aidé à fournir des solutions Web, mobiles et de bureau avec de nombreuses entreprises des secteurs de l’entreprise et du divertissement, notamment Adobe, THQ, Condé Nast Publications et Motorola.

Il écrit fréquemment sur les logiciels et la technologie sur son blog, soutient les logiciels Open Source et je dois faire de mon mieux pour redonner à la communauté du développement via GitHub et j’ai eu le plaisir estimé d’être co-auteur de plusieurs titres de O’Reilly et Wiley Wrox: profil Amazon.

Suivez Todd sur Twitter

Visitez le site de Todd

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.