BDD i JavaScript med CucumberJS

0

av Todd Anderson

jag har tidigare skrivit om test driven development (TDD) i JavaScript, framför allt med hjälp av behavior driven development (BDD) style library Jasmine i en serie om att bygga en testdriven Livsmedelslista ansökan. I den inläggserien gick jag igenom att tänka på användarhistorier för funktioner och scenarier som faktiska utvecklingsuppgifter och – läsa tillbaka på det – det är allt väldigt grönt (ingen ordspel avsedd) så långt jag hittar ett sätt att leverera testdriven kod. Det är inget fel med det och jag kommer sannolikt att titta på detta och efterföljande inlägg på samma sätt. Som sagt, Jag håller fortfarande sant att TDD är det bästa sättet att leverera kortfattad, testad och genomtänkt kod.

sedan dess har jag dock införlivat ett annat verktyg i mitt TDD-arbetsflöde för JavaScript-baserade projekt som ger mig integrationen av funktionsspecifikationer närmare min utveckling och verkligen omfattar mitt nuvarande ideal för Beteendedriven utveckling: CucumberJS. I huvudsak tillåter det mig att verkligen följa TDD medan jag utvecklar från utsidan i löpande automatiserade tester som misslyckas tills jag har skrivit kod som stöder en funktion.

antaganden och anteckningar

för exemplen i det här inlägget antas att du är bekant med NodeJS, npm, utveckla Nodmoduler och vanliga enhetstestmetoder eftersom dessa ämnen är för stora för att diskutera i det här inlägget.

stödfiler relaterade till detta ämne kommer att finnas tillgängliga på:
https://github.com/bustardcelly/cucumberjs-examples

CucumberJS

CucumberJS är en JavaScript-port för det populära BDD-verktyget gurka (som i sig var en omskrivning av RSpec). Det låter dig definiera funktionsspecifikationer i ett Domänspecifikt språk (DSL)-kallat Gherkin-och köra dina specifikationer med ett kommandoradsverktyg som rapporterar passering och/eller misslyckande av scenarier och de steg de består av.

det är viktigt att notera att gurka i sig inte tillhandahåller ett standardbibliotek. Det är ett testramverk som ger ett kommandoradsverktyg som förbrukar definierade funktioner och validerar scenarier genom att köra steg som är skrivna i JavaScript. Det är utvecklarens val att inkludera det önskade assertionsbiblioteket som används för att få dessa steg att passera eller misslyckas. Det är min avsikt att klargöra processen genom exempel genom en enda funktion med flera scenarier i det här inlägget.

Installation

du kan installera CucumberJS i ditt projekt med npm:

$ npm install cucumber --save-dev

Gherkin

om du hade följt med i den tidigare TDD-serien hittar du specifikationerna definierade i den serien som liknar Gherkin. Faktum är att jag kommer att hash en funktionsspecifikation från den serien för att visa att du arbetar genom din första cuke (aka, passing feature spec).

om vi skulle göra om inköpslistan under TDD / BDD med gurka, skulle vi först börja med en funktion med Gherkin-syntaxen:

/features/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 definierar ett affärsvärde, medan scenarierna definierar stegen som ger det värdet. Oftast i mjukvaruutvecklingsvärlden är det från dessa scenarier att utvecklingsuppgifter tas på och QA-test definieras.

jag slutade vid två scenarier, men vi kunde mycket enkelt lägga till fler scenarier till den här funktionen; omedelbart vad som kommer att tänka på är artikelinsättningsregler och validering av egenskaper som gör det möjligt för ett objekt att läggas till eller avvisas. I efterhand kan det vara mer meningsfullt att skapa separata funktionsspecifikationer för sådana detaljer. Vi kunde dock spendera ett helt inlägg på sådana ämnen, så låt oss komma tillbaka till den funktion som redan definierats.

inom varje Scenario finns en lista över sekventiella steg: Given, när och sedan. Det är dessa steg som CucumberJS kommer att utföra efter att ha konsumerat denna funktion spec. Efter var och en av dem kan du valfritt ha Och Och men, men – även om det är nödvändigt och oundvikligt ibland – försöker jag hålla mig borta från sådana ytterligare stegklausuler.

kör det

efter att ha sparat det till en fil i en / features-katalog kan vi sedan köra den under gurka:

$ node_modules/.bin/cucumber-js

som standard kommer CucumberJS att konsumera alla funktionsspecifikationer som finns i katalogen relative /features.

den aktuella konsolutgången kommer att se ut som följande, vilket i huvudsak betyder att alla steg inte har lokaliserats eller definierats:

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 odefinierade steg som utgör 2 scenarier och CucumberJS ci-verktyget ger även exempel på hur man definierar dem!

en viktig del av det snippet att förstå är att det bara finns 4 steg att implementera. I vår funktion har vi 2 Scenerios vardera med 3 steg. Det finns totalt 6 steg, men vi behöver bara definiera 4. Anledningen är att varje Scenario delar samma givna och när steg; dessa behöver bara definieras en gång och kommer att köras separat för varje Scenario. I huvudsak, om du definierar liknande steg med samma sammanhang, kommer det att återanvända ”setup” för ett enda steg inom varje Scenario.

jag använder ”setup” i citat eftersom jag menar det mer i rollen att definiera sammanhang för när och sedan steg.

jag vill inte bli förvirrad med installations – /teardown – metoderna för andra enhetstestmetoder-som är kända som före/efter supportuppgifter i CucumberJS-och bär mer av ett sammanhang för att skapa en miljö där tester sedan exekveras (som att fylla en DB av användare) och sedan riva ner den inställningen.

Stegdefinitioner

i föregående avsnitt såg vi att köra CucumberJS mot vår Lägg till Artikelfunktion varnade oss för att vi har odefinierade (och, men inte tryckta i rött, misslyckas) scenarier för att stödja funktionen. Som standard läser CucumberJS i alla funktioner från / features-katalogen i förhållande till var kommandot kördes, men det kunde inte hitta de STEP-filer som stöds där dessa metoder definieras.

som tidigare nämnts tillhandahåller CucumberJS inte ett assertion library. Det enda antagandet vid denna tidpunkt-eftersom CucumberJS-verktyget körs under NodeJS – är att de stödda stegen kommer att laddas som nodmoduler med en exporterad funktion som ska utföras. När vi börjar implementera stegen måste vi besluta om assertion library som ska användas för att validera vår logik. Vi lägger det beslutet på hyllan just nu och får barebones-installationen att misslyckas.

för att börja, låt oss ta de stegdefinitioner som tillhandahålls av CucumberJS ci-verktyget och släppa dem i en nodmodul:

/features/stepdefinitions/add-item.instruktion.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 kommer CucumberJS att leta efter steg som ska laddas i en mapp med titeln step_definitions under katalogen /features i förhållande till var du utfärdar kommandot. Du kan valfritt använda alternativet -r för att ladda CucumberJS steg från en annan plats. Att köra standard är detsamma som att ställa in alternativet för följande stegdefinitionskatalog:

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

konsolutgången kommer nu att se ut som följande:

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

inte för överraskande eftersom vi meddelar återuppringning av ett pending – tillstånd. CucumberJS går in i det första steget (givet) och returneras omedelbart med en väntande anmälan. Som sådan stör det inte med att ange några efterföljande steg och markerar dem som hoppade över.

Obs: Det är för mycket att komma in i en diskussion om klientsidan moduler och AMD vs CommonJS. I detta exempel kommer jag att använda CommonJS, som jag mina nuvarande intressen ligger i att använda Browserify för klientsidan utveckling. Under lång tid var jag förespråkare för RequireJS och AMD. Återigen, det är en helt annan diskussion.

Given

för att komma närmare grönt, kommer vi att ta itu med det givna steget först:

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

om vi skulle köra det igen skulle vi få ett undantag direkt:

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

vilket är förståeligt eftersom vi inte har definierat någon annan kod men denna stegdefinitionsmodul och försöker kräva en modul som inte existerar. Att hålla fast vid TDD är det bra-vi vet varför det misslyckas och vi förväntar oss det – jag skulle dra ut mitt hår om det inte kastade ett undantag!

för att få detta att passera skapar vi en Nodmodul i den angivna katalogen som exporterar ett objekt med en create metod:

/script/model/grocery-list.js

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

vi har gett minimikravet för att få vårt givna steg att passera. Vi kommer att oroa oss för detaljerna när vi närmar oss de senare stegen.

kör det igen, och CucumberJS går in i when-steget i varje Scenario och avbryter på grund av väntande retur.

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

när

i föregående avsnitt, för att få det givna steget att passera på varje Scenario implementerade vi början på en inköpslista modell genererad från en fabriksmetod, create, från modulen grocery-list. Jag vill inte komma in i en debatt om objektskapande, new – operatören, klasser och prototyper – åtminstone inte i det här inlägget-och antar att du är bekant och bekväm (åtminstone i läskod) med objekt.skapa definieras för ECMAScript 5.

vid granskning av när-steget för scenarierna:

When I add an item to the list

…vi måste tillhandahålla ett sätt att lägga till ett objekt i inköpslistan som skapats i den givna – och göra det i så liten kod för att göra steget passera.

först definierar vi vår förväntan på sminken och add underskrift av inköpslistan i stegdefinitionerna:

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

om vi kör det igen:

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

Oooo-weee! Nu pratar vi. Big, bright red F ’ S. brasilian

för att få det att komma tillbaka till passering, ändrar vi grocery-list med så lite kod som möjligt:

/script/modell / livsmedelsbutik-lista.js

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

kör igen, och CucumberJS har utvecklats till de dåvarande stegen som rapporterar ett pending – tillstånd.

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

sedan

har vi gått igenom våra stegimplementeringar och nått de steg där vi hävdar operationer och egenskaper som bevisar att vårt scenario ger sitt avsedda värde. Som tidigare nämnts, CucumberJS ger inte ett påstående bibliotek. Min preferens i assertion libraries är en kombination av Chai, Sinon och Sinon-Chai, men för exemplen i det här inlägget kommer jag bara att använda modulen assert som kommer med NodeJS. Jag uppmuntrar dig att kolla in andra assertion bibliotek och lämna en anteckning om du har en favorit.

Obs: Det här avsnittet kommer att vara ett litet exempel tungt eftersom vi snabbt byter från att ändra vår kod och kör spec runner ofta.

första scenariot

vid granskning av det första scenariot sedan steg:

Then The grocery list contains a single item

…vi måste bevisa att inköpslistan växer med en faktor 1 för varje ny artikel som läggs till.

uppdatera steget för att definiera hur vi förväntar oss att specifikationen ska valideras:

/feature/stepdefinitions/add-item.steg.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 dragit in assert – modulen och försökt validera att längden på inköpslistan har vuxit med ett värde av 1 efter att ha kört föregående steg – när-i att lägga till objektet.

kör det och vi får ett undantag:

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

Låt oss lägga till den metoden i vår inköpslista modell:

/script/modell / livsmedelsbutik-lista.js

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

och tillbaka till att köra våra SPECIFIKATIONER:

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

eftersom koden inte returnerar något från getAll() kan vi inte komma åt en length – egenskap för vårt assertionstest.

om vi ändrar koden för att returnera en Array:

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

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

och kör specifikationerna igen, vi får det påstående felmeddelande vi gav:

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

nu har vi ett ordentligt misslyckande som rapporteras till oss från ett påstående som gör att steget inte passerar. Hurra!

ta en andning

låt oss pausa här en sekund innan du lägger till mer kod för att få detta steg att passera. Problemet är inte att lägga till ett objekt i arrayen som returneras, det handlar mer om att se till att ett objekt läggs till genom add – metoden och resultatet från getAll är en lista som utökas med det objektet.

implementeringsdetaljerna som är involverade i att göra detta testpass är där ditt team använder sin arkitekturupplevelse, men det krävs att endast den viktigaste koden läggs till. Du bör undvika att gå överbord när du tänker på de inre delarna av modellen för inköpslista. Det är ett halt tätt rep som lätt kan falla ner i ett kaninhål – precis som den dåligt formulerade metaforen bisexuell

kom tillbaka till jobbet!

i det här exemplet använder vi argumentet propertiesObject för Object.create för att definiera en list getter som kommer att fungera som en muterbar array för våra inköpslistor:

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

om vi kör det kommer vi att upptäcka att det första scenariot nu passerar!

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

andra scenariot

vid granskning av det sista steget i vårt 2: a Scenario kommer den pågående implementeringen åt det tillagda objektet:

Then I can access that item from the grocery list

för att få detta steg att passera måste vi verifiera att vi kan komma åt objektet som bifogas inköpslistan genom att åberopa add() med ett objekt.

som med implementeringen av åtkomst till Inköpslistans längd finns det flera sätt på vilka vi kan få detta test att passera i koden. Återigen känner jag att det är här mjukvaruutveckling erfarenhet och smak spelar in när det gäller arkitektur, men jag föredrar också att försöka producera minsta möjliga kod; och jag kommer att vara den första som erkänner att jag ibland går lite frånvarande och skapar mer kod än vad som är nödvändigt… därför försöker.

som sagt måste vi också ta hänsyn till språk – och miljöspecifikationer i hur vi adresserar påståendet-och webbläsaren, med sin historia, har många att tänka på. Det är inte en liten, det är bara en eftertanke att ställa förväntningar på krav.

specifikt: Antag att vi skulle säga att steget kan verifieras med Array.indexOf() – metoden på samlingen som returneras från ’getAll()’ på inköpslistan? Utan polyfill begränsar vi oss till att skicka påståenden på IE 9 och äldre. Sådana överväganden är bara toppen av isberget när man beslutar om vad man ska införa i din kodbas för att få dina tester passera, och verkligen bör lämnas upp till ett team diskussion om vad som anses nödvändigt för att få produkten till produktion.

jag kunde fortsätta och fortsätta, men låt oss bara anta att vi vill täcka alla baser när det gäller webbläsare (IE 6 och uppåt, shudder). Enligt min mening, för att göra detta andra Scenario grönt, lägger vi till en getItemIndex() – metod med följande signatur:

+ getItemIndex(itemValue):int

vi ändrar först steget för att misslyckas:

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

godkännandet för att detta test ska passera är att indexet där det tillagda objektet finns i samlingen är icke-negativt. För det här scenariot försöker vi inte validera en specifikation om var ett nytt objekt läggs till i en lista (t.ex. prepended eller bifogad), utan helt enkelt att det är tillgängligt.

Running som kommer att producera ett undantag:

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

Låt oss ändra vårt Livsmedelslistaobjekt för att stödja getItemIndex – metoden:

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

i vår implementering av getItemIndex korsas listan och om objektet hittas returneras indexet. Annars returneras ett värde på -1. I huvudsak hur Array.indexOf – metoden fungerar i ECMAScript 5.

OBS: Jag vet att det kan tyckas dumt att använda objekt.skapa från ECMAScript 5, men inte Array.indexOf. Anledningen-mestadels-är att jag normalt alltid inkluderar en polyfill för objekt.skapa och inte för Array.indexOf. Jag antar vana.

nu om vi kör specifikationerna igen under CucumberJS:

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

våra cukes är gröna! (Detta är den punkt du torka pannan och långsam klappa).

slutsats

i det här inlägget introducerade jag hur jag använder BDD-verktyget CucumberJS för att följa testdriven utveckling i JavaScript. Jag gick igenom att använda ett exempel på en enda funktion med två scenarier och vända misslyckade steg till gröna cukes. Om du är obekant med processen med att göra tester misslyckas först bara för att producera kod för att göra testet passera, jag hoppas att du följde med; Jag kan vara ordig och processen kan tyckas ta mycket tid, men utvecklingen under sådana metoder börjar röra sig smidigt när du kommer i spåret. Dessutom tror jag att det finns en stor belöning i att ha din kod under en testsele när det gäller refactoring och buggfixning – både i utvecklarhälsa och företag.

den här artikeln publicerades ursprungligen på http://custardbelly.com/blog/blog-posts/2014/01/08/bdd-in-js-cucumberjs/index.html

bild med tillstånd av http://commons.wikimedia.org/wiki/File:Og%C3%B3rki…jpg

e6b2cff6a55928c8f62b9d602add3fed?s = 100D = mmr = g

Todd Anderson är en applikationsutvecklare med en passion för arkitektur och utvecklingsarbetsflöden.

som en stark förespråkare för agila metoder och testdriven utveckling med över ett decennium av erfarenhet har han hjälpt till att leverera webb -, mobil-och skrivbordslösningar med många företag inom företags-och underhållningsindustrin, inklusive Adobe, THQ, Cond Ukraianus Nast Publications och Motorola.

han skriver ofta om programvara och teknik på sin blogg, stöder öppen källkodsprogramvara och på grund av mitt bästa att ge tillbaka till utvecklingsgemenskapen via GitHub och har haft det uppskattade nöjet att vara medförfattare på flera titlar från O ’ Reilly och Wiley Wrox: Amazon profile.

följ Todd på Twitter

besök Todds webbplats

Lämna ett svar

Din e-postadress kommer inte publiceras.