BDD in JavaScript con CucumberJS

0

Da Todd Anderson

ho scritto in precedenza su test driven development (TDD) in JavaScript, più in particolare utilizzando il comportamento driven development (BDD) libreria di stili di Gelsomino in una serie sulla costruzione di un Test-Driven Lista della spesa di Applicazione. In quella serie di post ho pensato alle storie degli utenti per le funzionalità e gli scenari come attività di sviluppo reali, e – rileggendolo-è tutto molto verde (nessun gioco di parole) nella misura in cui sto trovando un modo per fornire codice basato sui test. Non c’è niente di sbagliato in questo e molto probabilmente guarderò questo e i post successivi allo stesso modo. Detto questo, continuo a ritenere vero che TDD è il modo migliore per fornire un codice conciso, testato e ben congegnato.

Da quel momento, tuttavia, ho incorporato uno strumento diverso nel mio flusso di lavoro TDD per progetti basati su JavaScript che mi offre l’integrazione delle specifiche delle funzionalità più strettamente al mio sviluppo e comprende veramente il mio attuale ideale di sviluppo guidato dal comportamento: CucumberJS. In sostanza, mi consente di aderire veramente a TDD mentre sviluppo dall’esterno test automatici in esecuzione che falliscono fino a quando non ho scritto codice che supporta una funzionalità.

Ipotesi e note

Per gli esempi in questo post, si presume che si abbia familiarità con NodeJS, npm, lo sviluppo di moduli di nodi e pratiche di test unitari comuni in quanto questi argomenti sono troppo grandi per essere discussi in questo post.

I file di supporto relativi a questo argomento saranno disponibili all’indirizzo:
https://github.com/bustardcelly/cucumberjs-examples

CucumberJS

CucumberJS è una porta JavaScript del popolare strumento BDD Cucumber (che a sua volta era una riscrittura di RSpec). Consente di definire le specifiche delle funzionalità in un linguaggio specifico del dominio (DSL), chiamato Gherkin, ed eseguire le specifiche utilizzando uno strumento a riga di comando che segnalerà il passaggio e/o il fallimento degli scenari e i passaggi da cui sono composti.

È importante notare che Cucumber stesso non fornisce una libreria di asserzione predefinita. È un framework di test che fornisce uno strumento a riga di comando che consuma funzionalità definite e convalida gli scenari eseguendo passaggi scritti in JavaScript. È la scelta degli sviluppatori di includere la libreria di asserzione desiderata utilizzata per far passare o fallire tali passaggi. È mio intento chiarire il processo con l’esempio attraverso una singola funzionalità con più scenari in questo post.

Installazione

Puoi installare CucumberJS nel tuo progetto usando npm:

$ npm install cucumber --save-dev

Gherkin

Se hai seguito la precedente serie TDD, troverai le specifiche definite in quella serie simili a Gherkin. In effetti, ri-hashing una specifica di funzionalità di quella serie per dimostrare di lavorare attraverso il tuo primo cuke (aka, passando le specifiche di funzionalità).

Se dovessimo rifare l’applicazione Lista della spesa sotto TDD/BDD usando Cucumber, inizieremmo prima con una funzione usando la sintassi Gherkin:

/features/add-item.feature

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 Feature definisce un valore aziendale, mentre gli Scenari definiscono i passaggi che forniscono tale valore. Molto spesso, nel mondo dello sviluppo software, è da questi Scenari che vengono eseguite le attività di sviluppo e vengono definiti i test QA.

Mi sono fermato a due Scenari, ma potremmo facilmente aggiungere più scenari a questa funzione; immediatamente ciò che viene in mente sono le regole di inserimento degli elementi e la convalida delle proprietà che consentono di aggiungere o rifiutare un elemento. Col senno di poi, potrebbe avere più senso creare specifiche di funzionalità separate per tali dettagli. Potremmo spendere un intero post su tali argomenti però, quindi torniamo alla funzione già definita.

All’interno di ogni scenario è un elenco di passaggi sequenziali: dato, quando e poi. Sono questi i passaggi che CucumberJS eseguirà dopo aver consumato questa specifica funzionalità. Dopo ognuno di questi, puoi opzionalmente avere And and But, tuttavia – anche se necessario e inevitabile a volte – cerco di stare lontano da tali clausole aggiuntive.

Eseguendolo

Dopo averlo salvato in un file in una directory / features, possiamo quindi eseguirlo sotto Cucumber:

$ node_modules/.bin/cucumber-js

Per impostazione predefinita, CucumberJS consumerà tutte le specifiche delle funzionalità trovate nella directory relative /features.

L’output corrente della console sarà simile al seguente, il che significa essenzialmente che tutti i passaggi non sono stati individuati o definiti:

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

Quindi abbiamo 6 Passaggi indefiniti che compongono 2 Scenari e lo strumento CucumberJS ci fornisce anche esempi su come definirli!

Una parte importante di quel frammento da capire è che ci sono solo 4 passaggi da implementare. Nella nostra caratteristica abbiamo 2 Scenerios ciascuno con 3 punti. Ci sono un totale di 6 passaggi, ma abbiamo solo bisogno di definire 4. Il motivo è che ogni scenario condivide lo stesso passo dato e Quando; questi devono essere definiti solo una volta e verranno eseguiti separatamente per ogni Scenario. In sostanza, se si definiscono Passaggi simili utilizzando lo stesso contesto, verrà riutilizzata la “configurazione” per un singolo passaggio all’interno di ogni scenario.

Uso “setup” tra virgolette perché lo intendo più nel ruolo di definire il contesto per Quando e poi i passaggi.

Non voglio confonderlo con i metodi di setup/teardown di altre pratiche di test unitari – che sono noti come attività di supporto Prima/Dopo in CucumberJS – e portano più di un contesto per impostare un ambiente in cui vengono quindi eseguiti i test (come riempire un DB di utenti) e quindi abbattere quel set up.

Definizioni dei passaggi

Nella sezione precedente, abbiamo visto che l’esecuzione di CucumberJS contro la nostra funzione Aggiungi elemento ci ha avvisato che abbiamo scenari indefiniti (e, anche se non stampati in rosso, falliti) per supportare la funzione. Per impostazione predefinita CucumberJS legge in tutte le funzionalità dalla directory / features relativa a dove è stato eseguito il comando, ma non è stato possibile individuare i file passo supportati in cui sono definiti questi metodi.

Come accennato in precedenza, CucumberJS non fornisce una libreria di asserzioni. L’unica ipotesi a questo punto – dal momento che lo strumento CucumberJS viene eseguito sotto NodeJS – è che i passaggi supportati verranno caricati come moduli nodo con una funzione esportata da eseguire. Mentre iniziamo a implementare i passaggi, dovremo decidere sulla libreria di asserzioni da utilizzare per convalidare la nostra logica. Metteremo questa decisione sullo scaffale al momento e faremo fallire l’installazione di barebone.

Per iniziare, prendiamo le definizioni dei passaggi fornite dallo strumento CucumberJS ci e le inseriamo in un modulo nodo:

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

Per impostazione predefinita, CucumberJS cercherà i passaggi da caricare all’interno di una cartella intitolata step_definitions nella directory /features relativa a dove si emette il comando. È possibile utilizzare opzionalmente l’opzione -r per far caricare i passaggi di CucumberJS da un’altra posizione. L’esecuzione del valore predefinito equivale all’impostazione dell’opzione directory definizione passo seguente:

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

L’output della console sarà ora simile al seguente:

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

Non troppo sorprendente visto che notifichiamo il callback di uno stato pending. CucumberJS entra nel primo passaggio (Dato) e viene immediatamente restituito con una notifica in sospeso. In quanto tale, non si preoccupa di inserire i passaggi successivi e li contrassegna come saltati.

Nota: È troppo entrare in una discussione sui moduli lato client e AMD vs CommonJS. Ai fini di questo esempio userò CommonJS, poiché i miei attuali interessi risiedono nell’utilizzo di Browserify per lo sviluppo lato client. Per molto tempo, sono stato un sostenitore di RequireJS e AMD. Ancora una volta, questa è tutta un’altra discussione.

Dato

Per avvicinarci al verde, affronteremo prima il passaggio dato:

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

Se dovessimo eseguirlo di nuovo, otterremmo subito un’eccezione:

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

Il che è comprensibile dal momento che non abbiamo definito nessun altro codice ma questo modulo di definizione del passo e stiamo cercando di richiedere un modulo che non esiste. Nell’attaccare con TDD, questa è una buona cosa – sappiamo perché sta fallendo e ce lo aspettiamo – mi tirerei fuori i capelli se non facesse un’eccezione!

Per farlo passare, creeremo un modulo Nodo nella directory specificata che esporta un oggetto con un metodo create:

/script/model/grocery-list.js

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

Abbiamo fornito il requisito minimo per ottenere il nostro passo dato per passare. Ci preoccuperemo dei dettagli mentre ci avviciniamo a questi ultimi passaggi.

Eseguilo di nuovo, e CucumberJS entra nel passaggio Quando di ogni scenario e si interrompe a causa del ritorno in sospeso.

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

Quando

Nella sezione precedente, per far passare il passaggio dato su ogni scenario, abbiamo implementato gli inizi di un modello di lista della spesa generato da un metodo di fabbrica, create, dal modulo grocery-list. Non voglio entrare in un dibattito sulla creazione di oggetti, l’operatore new, le classi e i prototipi – almeno non in questo post – e assumerò che tu sia familiare e a tuo agio (almeno nella lettura del codice) con l’oggetto.crea definito per ECMAScript 5.

Nel rivedere il passaggio Quando per gli scenari:

When I add an item to the list

…dobbiamo fornire un modo in cui aggiungere un elemento all’istanza della lista della spesa creata nel Dato – e farlo con il minimo codice per far passare il passaggio.

In primo luogo, definiremo la nostra aspettativa del make up eadd firma della lista della spesa nelle definizioni dei passaggi:

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

Se lo eseguiamo di nuovo:

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

Oooo-raee! Ora stiamo parlando. Grandi F rosse brillanti.

Per farlo tornare al passaggio, modificheremo grocery-list con il minor codice possibile:

/script/modello/alimentari-list.js

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

Esegui di nuovo e CucumberJS è passato ai passaggi Successivi che riportano uno stato pending.

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

Poi

Abbiamo progredito attraverso le nostre implementazioni passo e abbiamo raggiunto il passo(s) in cui affermiamo operazioni e proprietà che dimostrano che il nostro scenario fornisce il suo valore previsto. Come accennato in precedenza, CucumberJS non fornisce una libreria di asserzioni. La mia preferenza nelle librerie di asserzione è una combinazione di Chai, Sinon e Sinon-Chai, ma per gli esempi in questo post, userò solo il modulo assert fornito con NodeJS. Ti incoraggio a controllare altre librerie di asserzioni e lasciare una nota se hai un preferito.

Nota: Questa sezione sarà un piccolo esempio pesante mentre passiamo rapidamente dalla modifica del nostro codice ed eseguiamo frequentemente il corridore delle specifiche.

Primo scenario

Nell’esaminare il passaggio successivo del primo scenario:

Then The grocery list contains a single item

…dovremo dimostrare che l’istanza della lista della spesa cresce di un fattore 1 per ogni nuovo elemento aggiunto.

Aggiorna il passaggio per definire come ci aspettiamo che la specifica venga convalidata:

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

Abbiamo inserito il modulo assert e tentato di convalidare che la lunghezza della lista della spesa è cresciuta di un valore pari a 1 dopo aver eseguito il passaggio precedente – Quando – nell’aggiunta dell’elemento.

Eseguilo e otterremo un’eccezione:

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

Aggiungiamo questo metodo al nostro modello di lista della spesa:

/script/modello/alimentari-list.js

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

E torna a eseguire le nostre specifiche:

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

Visto che il codice non restituisce nulla da getAll(), non possiamo accedere a una proprietà length per il nostro test di asserzione.

Se modifichiamo il codice per restituire un Array:

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

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

Ed esegui di nuovo le specifiche, otterremo il messaggio di errore di asserzione che abbiamo fornito:

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

Ora, abbiamo un Errore corretto che ci viene segnalato da un’affermazione che fa sì che il passo non passi. Urrà!

Fai una pausa

Fermiamoci qui per un secondo prima di aggiungere altro codice per far passare questo passaggio. Il problema in questione non è in realtà l’aggiunta di un elemento all’array restituito, si tratta più di garantire che un elemento venga aggiunto tramite il metodo add e il risultato di getAll sia un elenco esteso con quell’elemento.

I dettagli di implementazione che sono coinvolti nel passaggio di questo test sono in cui il team utilizza la propria esperienza di architettura, ma è necessario assicurarsi che venga aggiunto solo il codice più essenziale. Dovresti evitare di esagerare nel pensare agli interni del modello di raccolta della lista della spesa. È una corda stretta scivolosa che potrebbe facilmente cadere in una tana di coniglio-proprio come quella metafora mal formulata

Torna al lavoro!

Ai fini di questo esempio, useremo l’argomento propertiesObject di Object.create per definire un getter list che fungerà da array mutabile per le nostre voci della lista della spesa:

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

Se lo eseguiamo, scopriremo che il primo scenario sta passando!

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

Secondo scenario

Nel rivedere la fase finale del nostro 2 ° scenario, l’implementazione in sospeso sta accedendo all’elemento aggiunto:

Then I can access that item from the grocery list

Per far passare questo passaggio dobbiamo verificare che possiamo accedere all’elemento aggiunto alla lista della spesa invocando add() con un elemento.

Come con l’implementazione di accedere alla lunghezza della lista della spesa, ci sono diversi modi in cui potremmo far passare questo test nel codice. Ancora una volta, sento che è qui che l’esperienza e il gusto dello sviluppo del software entrano in gioco per quanto riguarda l’architettura, ma preferisco anche cercare di produrre la minor quantità di codice possibile; e sarò il primo ad ammettere che a volte vado un po ‘ distratto e creo più codice del necessario hence quindi, provando.

Detto questo, dobbiamo anche prendere in considerazione le specifiche della lingua e dell’ambiente nel modo in cui affrontiamo il passaggio dell’asserzione – e il browser, con la sua cronologia, ha molti da considerare. Questo non è un leggero, è solo una accortezza nel fissare le aspettative per i requisiti.

In particolare: supponiamo di dover dire che il passaggio può essere verificato utilizzando il metodo Array.indexOf() sulla raccolta restituita da ‘getAll()’ sull’oggetto Lista della spesa? Senza un polyfill, allora ci stiamo limitando a passare asserzioni su IE 9 e più vecchi. Tali considerazioni sono solo la punta dell’iceberg quando si decide su cosa introdurre nella base di codice per far passare i test, e in realtà dovrebbero essere lasciati a una discussione di squadra su ciò che è considerato necessario per ottenere il prodotto in produzione.

Potrei andare avanti all’infinito, ma supponiamo di voler coprire tutte le basi quando si tratta di browser (IE 6 e versioni successive, rabbrividire). A mio parere, per rendere verde questo secondo Scenario, aggiungeremo un metodo getItemIndex() con la seguente firma:

+ getItemIndex(itemValue):int

Prima modificheremo il passaggio per fallire:

/feature/stepdefinitions / add-item.passo.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’accettazione per il superamento di questo test è che l’indice in cui l’elemento aggiunto risiede nella raccolta non è negativo. Per questo scenario, non stiamo cercando di convalidare una specifica su dove viene aggiunto un nuovo elemento in un elenco (ad esempio, anteposto o aggiunto), ma semplicemente che è accessibile.

Esecuzione che produrrà un’eccezione:

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

Modifichiamo il nostro oggetto Lista della spesa per supportare il metodo 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 } }); }};

Nella nostra implementazione di getItemIndex, l’elenco viene attraversato e, se viene trovato un elemento, viene restituito l’indice. In caso contrario, viene restituito un valore di -1. In sostanza, come funziona il metodo Array.indexOf di ECMAScript 5.

Nota: so che può sembrare sciocco usare Object.crea da ECMAScript 5, ma non Array.indexOf. La ragione – per lo più – è che normalmente includo sempre un polyfill per l’oggetto.crea e non per Array.indexOf. Suppongo abitudine.

Ora se eseguiamo nuovamente le specifiche sotto CucumberJS:

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

I nostri cukes sono VERDI! (Questo è il punto si pulisce la fronte e lento applauso).

Conclusione

In questo post, ho introdotto come uso lo strumento BDD CucumberJS per aderire allo sviluppo guidato dal test in JavaScript. Ho usato un esempio di una singola funzionalità con due scenari e ho trasformato i passaggi falliti in cukes verdi. Se non si ha familiarità con il processo con cui i test falliscono prima solo per produrre codice per far passare il test, spero che tu abbia seguito; Potrei essere prolisso e il processo potrebbe richiedere molto tempo, ma lo sviluppo in tali pratiche inizia a muoversi senza intoppi una volta entrato nel solco. Inoltre, penso che ci sia un’enorme ricompensa nell’avere il tuo codice sotto un cablaggio di prova quando si tratta di refactoring e correzione di bug, sia nella salute degli sviluppatori che nel business.

Questo articolo è stato originariamente pubblicato su http://custardbelly.com/blog/blog-posts/2014/01/08/bdd-in-js-cucumberjs/index.html

Immagine per gentile concessione di http://commons.wikimedia.org/wiki/File:Og%C3%B3rki…jpg

e6b2cff6a55928c8f62b9d602add3fed?s=100d=mmr = g

Todd Anderson è uno sviluppatore di applicazioni con la passione per l’architettura e i flussi di lavoro di sviluppo.

Come forte sostenitore di pratiche agili e di sviluppo guidato da test con oltre un decennio di esperienza, ha contribuito a fornire soluzioni web, mobili e desktop con numerose aziende nei settori enterprise e dell’intrattenimento, tra cui Adobe, THQ, Condé Nast Publications e Motorola.

Scrive spesso di software e tecnologia sul suo blog, supporta il software Open Source e devo fare del mio meglio per restituire alla comunità di sviluppo attraverso GitHub e ho avuto il piacere stimato di essere un co-autore di diversi titoli di O’Reilly e Wiley Wrox: Amazon profile.

Segui Todd su Twitter

Visita il sito di Todd

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.