BDD i JavaScript med CucumberJS

0

af Todd Anderson

jeg har tidligere skrevet om test driven development (TDD) i JavaScript, især ved hjælp af behavior driven development (BDD) style library Jasmine i en serie om opbygning af en testdrevet købmandsliste ansøgning. I den posts-serie gik jeg igennem at tænke på brugerhistorier for funktioner og scenarier som egentlige udviklingsopgaver, og – læsning tilbage på det – det hele er meget grønt (ingen ordspil beregnet), så vidt jeg finder en måde at levere testdrevet kode på. Der er ikke noget galt med det, og jeg vil sandsynligvis se på dette og efterfølgende indlæg på samme måde. Når det er sagt, holder jeg stadig fast, at TDD er den bedste måde at levere kortfattet, testet og gennemtænkt kode på.

siden da har jeg imidlertid indarbejdet et andet værktøj i min TDD-arbejdsgang til JavaScript-baserede projekter, der giver mig integrationen af funktionsspecifikationer tættere på min udvikling og virkelig omfatter mit nuværende ideal om Adfærdsdrevet udvikling: CucumberJS. I det væsentlige giver det mig mulighed for virkelig at overholde TDD, mens jeg udvikler udefra løbende automatiserede tests, der mislykkes, indtil jeg har skrevet kode, der understøtter en funktion.

antagelser og noter

for eksemplerne i dette indlæg antages det, at du er bekendt med NodeJS, npm, udvikling af Knudemoduler og fælles enhedstestpraksis, da disse emner er for store til at diskutere i dette indlæg.

understøttende filer relateret til dette emne vil være tilgængelige på:
https://github.com/bustardcelly/cucumberjs-examples

CucumberJS

CucumberJS er en JavaScript-port af det populære BDD-værktøj agurk (som i sig selv var en omskrivning af RSpec). Det giver dig mulighed for at definere funktionsspecifikationer i et Domænespecifikt sprog (DSL)-kaldet Gherkin-og køre dine specifikationer ved hjælp af et kommandolinjeværktøj, der rapporterer, at scenarier passerer og/eller fejler, og de trin, de består af.

det er vigtigt at bemærke, at agurk selv ikke giver en standard påstand bibliotek. Det er en testramme, der giver et kommandolinjeværktøj, der bruger definerede funktioner og validerer scenarier ved at køre trin, der er skrevet i JavaScript. Det er udviklerens valg at inkludere det ønskede påstandsbibliotek, der bruges til at få disse trin til at passere eller mislykkes. Det er min hensigt at afklare processen ved eksempel gennem en enkelt funktion med flere scenarier i dette indlæg.

Installation

du kan installere CucumberJS i dit projekt ved hjælp af npm:

$ npm install cucumber --save-dev

Gherkin

hvis du havde fulgt med i den forrige TDD-serie, finder du de specifikationer, der er defineret i den serie, der ligner Gherkin. Faktisk vil jeg re-hashing en funktion spec fra den serie for at demonstrere at arbejde gennem din første cuke (aka, passing feature spec).

hvis vi skulle genskabe købmandslisten under TDD/BDD ved hjælp af agurk, ville vi først starte med en funktion ved hjælp af Gherkin-syntaksen:

/funktioner/add-item.funktion

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

funktionen definerer en forretningsværdi, mens scenarierne definerer de trin, der giver denne værdi. Oftest er det i programmeludviklingsverdenen ud fra disse scenarier, at udviklingsopgaver tages på, og kvalitetstest defineres.

jeg stoppede ved to scenarier, men vi kunne meget let tilføje flere scenarier til denne funktion; straks hvad der kommer til at tænke på er elementindsættelsesregler og validering af egenskaber, der gør det muligt at tilføje eller afvise et element. Efterhånden kan det være mere fornuftigt at oprette separate funktionsspecifikationer for sådanne detaljer. Vi kunne dog bruge et helt indlæg på sådanne emner, så lad os vende tilbage til den allerede definerede funktion.

inden for hvert scenarie er en liste over sekventielle trin: givet, hvornår og da. Det er disse trin, at CucumberJS vil udføre efter at have forbrugt denne funktion spec. Efter hver af dem, du kan valgfrit have og Og men, dog – selvom det er nødvendigt og uundgåeligt til tider – jeg prøver at holde mig væk fra sådanne yderligere trinklausuler.

kører det

efter at have gemt det ned til en fil i en / Funktioner mappe, kan vi derefter køre det under agurk:

$ node_modules/.bin/cucumber-js

som standard vil CucumberJS forbruge alle funktionsspecifikationer, der findes i mappen relativ /funktioner.

den aktuelle konsoludgang vil se noget ud som det følgende, hvilket i det væsentlige betyder, at alle trin ikke er placeret eller defineret:

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

så vi har 6 udefinerede trin, der udgør 2 scenarier, og CucumberJS ci-værktøjet giver endda eksempler på, hvordan man definerer dem!

en vigtig del af det uddrag at forstå er, at der kun er 4 trin at implementere. I Vores funktion har vi 2 Scenerios hver med 3 trin. Der er i alt 6 trin, men vi behøver kun at definere 4. Årsagen er, at hvert scenarie deler det samme givne og hvornår trin; disse skal kun defineres en gang og køres separat for hvert scenarie. I det væsentlige, hvis du definerer lignende trin ved hjælp af den samme kontekst, vil den genbruge “opsætningen” til et enkelt trin inden for hvert Scenario.

jeg bruger “setup” i citater, fordi jeg mener det mere i rollen som at definere kontekst for hvornår og derefter trin.

jeg ønsker ikke at få det forvirret med opsætningen/nedrivningsmetoderne for andre enhedstestpraksis – som er kendt som før/efter supportopgaver i CucumberJS – og bærer mere af en kontekst til opsætning af et miljø, hvor test derefter udføres (som f.eks.

Trindefinitioner

i det foregående afsnit så vi, at kørsel af CucumberJS mod vores tilføjelsesfunktion advarede os om, at vi har udefinerede (og dog ikke trykt i rødt, svigtende) scenarier til understøttelse af funktionen. Som standard læser CucumberJS i alle funktioner fra mappen /funktioner i forhold til hvor kommandoen blev kørt, men den kunne ikke finde de understøttede trinfiler, hvor disse metoder er defineret.

som tidligere nævnt giver CucumberJS ikke et påstandsbibliotek. Den eneste antagelse på dette tidspunkt-da CucumberJS-værktøjet køres under NodeJS – er, at de understøttede trin indlæses som knudemoduler med en eksporteret funktion, der skal udføres. Når vi begynder at implementere trinnene, bliver vi nødt til at beslutte, om assertion library skal bruges til at validere vores logik. Vi sætter denne beslutning på hylden i øjeblikket og får barebones-opsætningen til at mislykkes.

for at starte, lad os tage de trindefinitioner, der leveres af CucumberJS ci-værktøjet, og slip dem i et knudemodul:

/funktioner/stepdefinitioner/add-item.trappe.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(); });};

som standard vil CucumberJS se efter trin, der skal indlæses i en mappe med titlen step_definitions under mappen /funktioner i forhold til hvor du udsteder kommandoen. Du kan eventuelt bruge -r mulighed for at have CucumberJS indlæse trin fra et andet sted. At køre standard er det samme som at indstille følgende trindefinitionskatalogindstilling:

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

konsoludgangen vil nu se ud som følgende:

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

ikke for overraskende, da vi meddeler tilbagekaldelsen af en pending tilstand. CucumberJS går ind i det første trin (givet) og returneres straks med en afventende anmeldelse. Som sådan gider det ikke med at indtaste efterfølgende trin og markerer dem som sprunget over.

Bemærk: Det er for meget at komme ind i en diskussion om klientmoduler og AMD vs CommonJS. Med henblik på dette eksempel vil jeg bruge CommonJS, da jeg mine nuværende interesser ligger i at bruge til udvikling af klientsiden. I lang tid var jeg en fortaler for Kræv og AMD. Igen er det en helt anden diskussion.

givet

for at komme tættere på grønt, tackler vi det givne trin først:

/funktioner/stepdefinitioner/add-item.trin.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(); }); ...};

hvis vi skulle køre det igen, ville vi få en undtagelse med det samme:

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

hvilket er forståeligt, da vi ikke har defineret nogen anden kode, men dette trindefinitionsmodul og forsøger at kræve et modul, der ikke findes. Ved at holde fast i TDD er det en god ting – vi ved, hvorfor det fejler, og vi forventer det – jeg ville trække mit hår ud, hvis det ikke kastede en undtagelse!

for at få dette til at passere, opretter vi et Knudemodul i den angivne mappe, der eksporterer et objekt med en create metode:

/script/model/købmandsliste.js

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

vi har givet det minimale krav for at få vores givne skridt til at passere. Vi vil bekymre os om detaljerne, når vi nærmer os de sidstnævnte trin.

kør det igen, og CucumberJS går ind i hvornår trin i hvert Scenario og afbryder på grund af afventende retur.

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

når

i det foregående afsnit implementerede vi begyndelsen på en Købmandslistemodel genereret fra en fabriksmetode, create, fra modulet grocery-list. Jeg ønsker ikke at komme ind i en debat om objektoprettelse, new operatør, klasser og prototyper – i det mindste ikke i dette indlæg – og vil antage, at du er fortrolig og komfortabel (i det mindste i læsekode) med objekt.Opret defineret for ECMAScript 5.

ved gennemgang af Hvornår trin for scenarierne:

When I add an item to the list

…vi er nødt til at give en måde, hvorpå vi kan tilføje en vare til købmandslisten, der er oprettet i det givne – og gøre det i så lidt kode for at få trinnet til at passere.

først definerer vi vores forventning om make up ogadd underskrift af købmandslisten i trindefinitionerne:

/funktioner/stepdefinitioner/add-item.trin.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(); }); ...};

hvis vi kører det igen:

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

oooo! Nu taler vi. Store, lyse røde F ‘ er. Larsen

for at få det til at komme tilbage til at passere, ændrer vi grocery-list med så lidt kode som muligt:

/script/model / købmand-liste.JS

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

Kør igen, og CucumberJS er gået videre til de daværende trin, der rapporterer en pending tilstand.

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

derefter

vi gik videre gennem vores trinimplementeringer og har nået de trin, hvor vi hævder operationer og egenskaber, der beviser, at vores scenarie giver den tilsigtede værdi. Som tidligere nævnt, CucumberJS giver ikke en påstand bibliotek. Min præference i assertion libraries er en kombination af Chai, Sinon og Sinon-Chai, men for eksemplerne i dette indlæg vil jeg bare bruge assert modulet, der følger med NodeJS. Jeg opfordrer dig til at tjekke andre påstandsbiblioteker og efterlade en note, hvis du har en favorit.

Bemærk: dette afsnit vil være et lille eksempel tungt, da vi hurtigt skifter fra at ændre vores kode og kører spec runner ofte.

første scenarie

ved gennemgang af det første Scenariums derefter trin:

Then The grocery list contains a single item

…vi bliver nødt til at bevise, at købmandslisten forekomst vokser med en faktor på 1 for hver ny vare tilføjet.

Opdater trinnet for at definere, hvordan vi forventer, at specifikationen skal valideres:

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

vi har trukket i assert modulet og forsøgt at validere, at længden af købmandslisten er vokset med en værdi på 1 efter at have kørt det forrige trin – når – i at tilføje varen.

kør det, og vi får en undtagelse:

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

lad os tilføje denne metode til vores købmandsliste model:

/script/model / købmand-liste.js

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

og tilbage til at køre vores specs:

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

da koden ikke returnerer noget fra getAll(), kan vi ikke få adgang til en length ejendom til vores påstandstest.

hvis vi ændrer koden for at returnere et Array:

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

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

og kør specifikationerne igen, vi får den påstandsfejl, vi har angivet:

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

nu har vi en ordentlig fejl, der rapporteres til os fra en påstand, der får skridtet til ikke at passere. Hurra!

tag en pause

lad os holde pause her et øjeblik, før vi tilføjer mere kode for at få dette trin til at passere. Problemet ved hånden er faktisk ikke at tilføje en vare til arrayet, der returneres, det handler mere om at sikre, at en vare tilføjes gennem add – metoden, og resultatet fra getAll er en liste udvidet med den vare.

implementeringsdetaljerne, der er involveret i at lave dette testkort, er der, hvor dit team bruger deres arkitekturoplevelse, men det kræves, at kun den mest vigtige kode tilføjes. Du bør undgå at gå overbord i at tænke på internals af indkøbsliste samling model. Det er et glat, stramt reb, der let kunne falde ned i et kaninhul – ligesom den dårligt formulerede metafor Kristian

kom tilbage til arbejde!

med henblik på dette eksempel bruger vi argumentet propertiesObject af Object.create til at definere en list getter, der vil fungere som et foranderligt array for vores indkøbsliste:

/script/model/købmandsliste.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 } }); }};

hvis vi kører det, finder vi ud af, at det første Scenario nu går forbi!

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

andet scenarie

ved gennemgang af det sidste trin i vores 2. scenarie får den verserende implementering adgang til det tilføjede element:

Then I can access that item from the grocery list

for at få dette trin til at passere skal vi kontrollere, at vi kan få adgang til varen, der er vedlagt købmandslisten, ved at påberåbe add() med en vare.

som med implementeringen af adgang til længden af købmandslisten er der flere måder, hvorpå vi kunne få denne testpas i koden. Igen føler jeg, at det er her, hvor programmeludviklingserfaring og smag kommer i spil med hensyn til arkitektur, men jeg foretrækker også at forsøge at producere mindst mulig kode; og jeg vil være den første til at indrømme, at jeg nogle gange går lidt fraværende og skaber mere kode end nødvendigt… derfor forsøger jeg.

når det er sagt, er vi også nødt til at tage hensyn til sprog – og miljøspecifikationer i, hvordan vi adresserer at gøre påstanden pass-og bro.sereren med sin historie har mange at overveje. Det er ikke en lille, det er bare en omtanke i at sætte forventninger til krav.

specifikt: Antag, at vi skulle sige, at trinnet kan verificeres ved hjælp af Array.indexOf() – metoden på samlingen returneret fra ‘getAll()’ på Købmandslisteobjektet? Uden en polyfill, så begrænser vi os til at videregive påstande om IE 9 og ældre. Sådanne overvejelser er kun toppen af isbjerget, når man beslutter, hvad man skal introducere i din kodebase for at få dine tests bestået, og bør virkelig overlades til en holddiskussion om, hvad der anses for nødvendigt for at få produktet til produktion.

jeg kunne fortsætte og fortsætte, men lad os bare antage, at vi vil dække alle baser, når det kommer til bro.sere (dvs. 6 og op, gyse). Efter min mening, for at gøre dette andet Scenario grønt, vil vi tilføje en getItemIndex() metode med følgende signatur:

+ getItemIndex(itemValue):int

vi ændrer først trinnet til at mislykkes:

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

accepten for at denne test skal bestå er, at indekset, hvor den tilføjede vare ligger i samlingen, ikke er negativ. I dette scenarie forsøger vi ikke at validere en specifikation for, hvor et nyt element tilføjes på en liste (f.eks.

løb, der vil producere en undtagelse:

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

lad os ændre vores indkøbsliste objekt til at understøtte getItemIndex metode:

/script/model/købmand-liste.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 } }); }};

i vores implementering af getItemIndex krydses listen, og hvis varen findes, returneres indekset. Ellers returneres en værdi på -1. I det væsentlige, hvordan Array.indexOf – metoden fungerer i ECMAScript 5.

Bemærk: Jeg ved, det kan virke fjollet at bruge objekt.Opret fra ECMAScript 5, men ikke Array.indeks. Årsagen – for det meste-er, at jeg normalt altid inkluderer en polyfill til objekt.Opret og ikke for Array.indeks. Jeg formoder vane.

nu hvis vi kører specs igen under CucumberJS:

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

vores cukes er grønne! (Dette er det punkt, du tørrer din pande og langsomt klapper).

konklusion

i dette indlæg introducerede jeg, hvordan jeg bruger BDD-værktøjet CucumberJS for at overholde testdrevet udvikling i JavaScript. Jeg gik igennem ved hjælp af et eksempel på en enkelt funktion med to scenarier og dreje svigtende trin til grønne cukes. Hvis du ikke er bekendt med processen med at lave test mislykkes først kun for at producere kode for at få testpasset, jeg håber du fulgte med; Jeg kan være ordrig, og processen kan synes at tage meget tid, men udvikling under sådan praksis begynder at bevæge sig glat, når du kommer i rillen. Derudover tror jeg, at der er en stor belønning i at have din kode under en testsele, når det kommer til refactoring og fejlrettelse – både i udviklerens sundhed og forretning.

denne artikel blev oprindeligt sendt på http://custardbelly.com/blog/blog-posts/2014/01/08/bdd-in-js-cucumberjs/index.html

Billede venligst udlånt af http://commons.wikimedia.org/wiki/File:Og%C3%B3rki…jpg

e6b2cff6a55928c8f62b9d602add3fed?s=100D=mmr=g

Todd Anderson er en applikationsudvikler med en passion for arkitektur og udviklingsarbejdsgange.

som en stærk fortaler for agile praksisser og testdrevet udvikling med over ti års erfaring har han hjulpet med at levere internet -, mobil-og desktop-løsninger med adskillige virksomheder inden for forretnings-og underholdningsindustrien, herunder Adobe, Condrius Nast Publications og Motorola.

han skriver ofte om programmel og teknologi på sin blog, støtter Open Source-programmel og på grund af mit bedste for at give tilbage til udviklingssamfundet gennem GitHub og har haft den værdsatte fornøjelse at være medforfatter på flere titler fra O ‘ Reilly.

Følg Todd på kvidre

besøg Todds hjemmeside

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.