BDD in JavaScript met Komkommerjs

0

door Todd Anderson

ik heb eerder geschreven over test driven development (TDD) in JavaScript, met name met behulp van de behavior driven development (BDD) stijl bibliotheek Jasmine in een serie over het bouwen van een Test-Driven Boodschappenlijstapplicatie. In die berichten serie ging ik door het denken van de gebruiker verhalen voor functies en scenario ‘ s als werkelijke ontwikkeling taken, en – het lezen terug op het – het is allemaal erg groen (geen woordspeling bedoeld) voor zover ik het vinden van een manier om te leveren test-driven code. Er is niets mis mee en Ik zal waarschijnlijk kijken naar deze en volgende posten op dezelfde manier. Dat gezegd hebbende, ik houd nog steeds waar dat TDD is de beste manier om beknopte, geteste en goed doordachte code te leveren.

sinds die tijd heb ik echter een andere tool in mijn TDD-workflow voor JavaScript-gebaseerde projecten opgenomen die me de integratie van feature specs dichter bij mijn ontwikkeling biedt en echt mijn huidige ideaal van gedrag gedreven ontwikkeling omvat: CucumberJS. In wezen, het stelt me in staat om echt te houden aan TDD tijdens het ontwikkelen van de buitenkant in – running geautomatiseerde tests die mislukken totdat Ik code die een functie ondersteunt geschreven.

aannames en opmerkingen

voor de voorbeelden in dit artikel wordt aangenomen dat u bekend bent met NodeJS, npm, het ontwikkelen van Knooppuntmodules en gemeenschappelijke testmethoden voor eenheden, omdat deze onderwerpen te groot zijn om in dit artikel te bespreken.

ondersteunende bestanden met betrekking tot dit onderwerp zullen beschikbaar zijn op:
https://github.com/bustardcelly/cucumberjs-examples

CucumberJS

CucumberJS is een JavaScript port van de populaire BDD tool Cucumber (die zelf een herschrijving van RSpec was). Hiermee kunt u Feature Specs te definiëren in een domein-specifieke taal (DSL)-genaamd Gherkin – en voer uw specs met behulp van een command line tool die het passeren en/of falen van scenario ‘ s en de stappen die ze bestaan uit zal rapporteren.

het is belangrijk op te merken dat Cucumber zelf geen standaard assertiebibliotheek biedt. Het is een testing framework biedt een command line tool die gedefinieerde functies verbruikt en valideert scenario ‘ s door het uitvoeren van stappen die zijn geschreven in JavaScript. Het is de keuze van de ontwikkelaars om de gewenste assertie bibliotheek te gebruiken om deze stappen te laten passeren of mislukken. Het is mijn bedoeling om het proces te verduidelijken door middel van een enkele functie met meerdere scenario ‘ s in dit bericht.

installatie

u kunt CucumberJS in uw project installeren met npm:

$ npm install cucumber --save-dev

Gherkin

Als u de vorige TDD-serie had gevolgd, zult u de specificaties die in die serie zijn gedefinieerd, vergelijkbaar vinden met Gherkin. In feite, Ik zal opnieuw hashing een feature spec uit die serie aan te tonen werken door middel van uw eerste cuke (aka, passeren feature spec).

als we de Boodschappenlijsttoepassing onder TDD/BDD opnieuw zouden maken met behulp van komkommer, zouden we eerst beginnen met een functie die de augurk syntaxis gebruikt:

/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

de Feature definieert een bedrijfswaarde, terwijl de scenario ‘ s de stappen definiëren die die waarde opleveren. Meestal, in de software-ontwikkelingswereld, is het vanuit deze scenario ‘ s dat ontwikkelingstaken worden genomen en QA-tests worden gedefinieerd.

ik ben gestopt bij twee scenario ‘s, maar we kunnen heel gemakkelijk meer scenario’ s toevoegen aan deze functie; onmiddellijk wat in gedachten komt zijn item invoegregels en validatie van eigenschappen die het mogelijk maken om een item toe te voegen of te weigeren. Achteraf gezien, het zou zinvoller zijn om afzonderlijke feature specs voor dergelijke details te maken. We kunnen een hele post besteden aan dergelijke onderwerpen, dus laten we terug naar de functie al gedefinieerd.

binnen elk Scenario is een lijst van opeenvolgende stappen: gegeven, wanneer en dan. Het zijn deze stappen die CucumberJS zal uitvoeren na deze functie spec hebben verbruikt. Na elk van deze, kunt u optioneel hebben en en maar, echter – hoewel noodzakelijk en onvermijdelijk Soms – Ik probeer weg te blijven van dergelijke extra stap clausules.

het draaien

nadat het is opgeslagen in een bestand in een map /features, kunnen we het dan uitvoeren onder komkommer:

$ node_modules/.bin/cucumber-js

standaard zal CucumberJS alle feature specs in de relatieve /features directory gebruiken.

de huidige console-uitvoer zal er ongeveer als volgt uitzien, wat in wezen betekent dat niet alle stappen zijn gelokaliseerd of gedefinieerd:

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

dus we hebben 6 ongedefinieerde stappen die 2 scenario ‘ s vormen en de CucumberJS ci tool geeft zelfs voorbeelden voor hoe ze te definiëren!

een belangrijk deel van dat fragment om te begrijpen is dat er slechts 4 stappen te implementeren zijn. In onze functie hebben we 2 Scenerios elk met 3 stappen. Er zijn in totaal 6 stappen, maar we hoeven slechts 4 te definiëren. De reden is dat elk Scenario dezelfde gegeven en wanneer stap deelt; deze hoeven slechts eenmaal te worden gedefinieerd en zullen afzonderlijk worden uitgevoerd voor elk Scenario. In wezen, als u soortgelijke stappen te definiëren met behulp van dezelfde context, zal het opnieuw de “setup” voor een enkele stap binnen elk Scenario.

Ik gebruik “setup” tussen aanhalingstekens omdat ik het meer bedoel in de rol van het definiëren van context voor wanneer en dan stappen.

Ik wil het niet verwarren met de setup/teardown methoden van andere unit testing praktijken – die bekend staan als voor/na ondersteuning taken in Komkommerjs – en meer een context hebben voor het opzetten van een omgeving waarin tests worden uitgevoerd (zoals het vullen van een DB van gebruikers) en vervolgens het afbreken van die set-up.

Stapdefinities

in de vorige sectie zagen we dat het draaien van Komkommerjs tegen onze Add Item-functie ons waarschuwde dat we ongedefinieerde (en, hoewel niet in rood afgedrukt, falende) scenario ‘ s hebben om de functie te ondersteunen. Standaard leest CucumberJS alle functies uit de map /features ten opzichte van waar het commando werd uitgevoerd, maar het kon niet de ondersteunde step-bestanden vinden waarin deze methoden zijn gedefinieerd.

zoals eerder vermeld, biedt CucumberJS geen assertiebibliotheek. De enige veronderstelling op dit punt – aangezien de CucumberJS tool wordt uitgevoerd onder NodeJS – is dat de ondersteunde stappen worden geladen als knooppunt modules met een geëxporteerde functie uit te voeren. Als we beginnen met het implementeren van de stappen, zullen we moeten beslissen over de assertion library te gebruiken bij het valideren van onze logica. We zetten die beslissing nu op de plank en zorgen dat de barebones setup mislukt.

om te beginnen, nemen we de stapdefinities die door de cucumberjs ci tool worden gegeven en drop ze in een knooppuntmodule:

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

standaard zal CucumberJS zoeken naar stappen die geladen moeten worden in een map met de titel step_definitions onder de map /features relatief tot waar u het commando uitvoert. U kunt optioneel de -r optie gebruiken om Komkommerjs stappen te laten laden vanaf een andere locatie. Het uitvoeren van de standaard is hetzelfde als het instellen van de volgende stap definitie directory optie:

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

de console-uitvoer ziet er nu als volgt uit:

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

niet al te verrassend aangezien we de callback van een pending status melden. Komkommerjs voert de eerste stap (gegeven) en wordt onmiddellijk terug met een hangende kennisgeving. Als zodanig, het doet geen moeite met het invoeren van eventuele volgende stappen en markeert ze als overgeslagen.

Opmerking: Het is te veel om in discussie te gaan over modules aan de client-kant en AMD vs CommonJS. Voor de doeleinden van dit voorbeeld zal Ik gebruik maken van CommonJS, als ik mijn huidige belangen liggen in het gebruik van Browserify voor client-side ontwikkeling. Lange tijd was ik een voorstander van RequireJS en AMD. Nogmaals, dat is een heel andere discussie.

gegeven

om dichter bij Groen te komen, zullen we de gegeven stap eerst aanpakken:

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

als we dat opnieuw zouden uitvoeren, zouden we meteen een uitzondering krijgen:

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

wat begrijpelijk is omdat we geen andere code hebben gedefinieerd maar deze step definition module en proberen een module te vereisen die niet bestaat. In het vasthouden aan TDD, dit is een goede zaak – we weten waarom het faalt en we verwachten het – Ik zou mijn haar uit te trekken als het niet gooien van een uitzondering!

om dit te laten slagen, maken we een Knooppuntmodule aan in de opgegeven map die een object exporteert met een create methode:

/script/model/grocery-list.js

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

we hebben de minimale vereiste gegeven om onze gegeven stap te laten slagen. We zullen ons zorgen maken over de details als we de laatste stappen naderen.

voer dat opnieuw uit, en Komkommerjs komt binnen in de “When” – stap van elk Scenario en stopt vanwege de terugkeer in afwachting.

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

wanneer

in de vorige paragraaf, om de gegeven stap door te geven op elk Scenario hebben we het begin van een Boodschappenlijstmodel geïmplementeerd dat is gegenereerd op basis van een fabrieksmethode, create, uit de grocery-list module. Ik wil niet in een debat komen over object creatie ,de new operator, klassen en prototypes-tenminste niet in deze post – en zal aannemen dat je vertrouwd en comfortabel bent (tenminste in het lezen van code) met Object.maak gedefinieerd voor ECMAScript 5.

bij het herzien van de “When” – stap voor de scenario ‘ s:

When I add an item to the list

…we moeten een manier bieden om een item toe te voegen aan de Grocery List instantie gemaakt in de gegeven – en doe dit in zo weinig code om de stap te laten passeren.

eerst definiëren we onze verwachting van de samenstelling en add ondertekening van de boodschappenlijst in de step definities:

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

als we dat opnieuw uitvoeren:

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

Oooo-weee! Nu kunnen we praten. Grote, felrode F ‘ S. 🙂

om dat weer te laten overgaan, zullen we grocery-list aanpassen met zo weinig mogelijk code:

/ script / model / grocery-list.js

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

opnieuw uitvoeren, en Komkommerjs is gevorderd tot de stappen die een pending status rapporteren.

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

vervolgens

hebben we de stap(en) bereikt waarop we bewerkingen en eigenschappen uitvoeren die bewijzen dat ons scenario de beoogde waarde heeft. Zoals eerder vermeld, biedt CucumberJS geen assertiebibliotheek. Mijn voorkeur in assertion libraries is een combinatie van Chai, Sinon en Sinon-Chai, maar voor de voorbeelden in dit bericht, ga ik gewoon de assert module gebruiken die bij NodeJS wordt geleverd. Ik moedig je aan om andere assertiebibliotheken te bekijken en een briefje achter te laten als je een favoriet hebt.

Opmerking: Deze sectie zal een klein voorbeeld zwaar zijn als we snel overschakelen van het wijzigen van onze code en de spec runner vaak draaien.

eerste Scenario

bij de herziening van de eerste Scenario ‘ s en vervolgens stap:

Then The grocery list contains a single item

…we zullen moeten bewijzen dat de Boodschappenlijstinstantie groeit met een factor 1 voor elk nieuw item toegevoegd.

werk de stap bij om te bepalen hoe we verwachten dat de specificatie wordt gevalideerd:

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

we hebben de module assert opgehaald en geprobeerd te bevestigen dat de lengte van de boodschappenlijst is gegroeid met een waarde van 1 na het uitvoeren van de vorige stap – wanneer – in het toevoegen van het item.

voer dat uit en we krijgen een uitzondering:

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

laten we die methode toevoegen aan ons boodschappenlijstje model:

/ script / model / grocery-list.js

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

en terug naar het uitvoeren van onze specs:

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

aangezien de code niets retourneert van getAll(), hebben we geen toegang tot een length eigenschap voor onze assertietest.

als we de code wijzigen om een Array te retourneren:

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

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

en voer de specs opnieuw uit, we krijgen de bevestiging foutmelding die we hebben gegeven:

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

nu hebben we een behoorlijke fout die aan ons wordt gemeld door een bewering die ervoor zorgt dat de stap niet doorgaat. Hoera!

neem een adempauze

laten we hier even pauzeren voordat we meer code toevoegen om deze stap te laten passeren. Het gaat hier niet om het toevoegen van een item aan de array die wordt geretourneerd, het gaat er meer om ervoor te zorgen dat een item wordt toegevoegd via de add methode en dat het resultaat van getAll een lijst is die met dat item wordt uitgebreid.

de implementatie details die betrokken zijn bij het maken van deze test pass zijn waar uw team gebruik maakt van hun architectuur ervaring, maar zorg is vereist dat alleen de meest essentiële code wordt toegevoegd. Je moet voorkomen dat overboord te gaan in het denken over de binnenkant van de Boodschappenlijstcollectie model. Het is een glad strak touw dat gemakkelijk in een konijnenhol kan vallen-net als die slecht geformuleerde metafoor 🙂

aan het werk!

in dit voorbeeld gebruiken we het propertiesObject argument van Object.create om een list getter te definiëren die zal dienen als een veranderlijke array voor onze boodschappenlijstitems:

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

als we dat uitvoeren, zullen we merken dat het eerste Scenario nu voorbij is!

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

tweede Scenario

bij de herziening van de laatste stap van ons tweede Scenario, is de lopende implementatie toegang tot het toegevoegde item:

Then I can access that item from the grocery list

om deze stap door te laten gaan moeten we controleren of we toegang hebben tot het item dat aan de boodschappenlijst is toegevoegd door add() aan te roepen met een item.

net als bij de implementatie van toegang tot de lengte van de boodschappenlijst, zijn er verschillende manieren waarop we deze test kunnen laten slagen in de code. Nogmaals, ik voel dat dit is waar software ontwikkeling ervaring en smaak komt in het spel met betrekking tot architectuur, maar ik heb ook liever proberen om de minste hoeveelheid code mogelijk te produceren; en Ik zal de eerste zijn om toe te geven dat soms ga ik een beetje verstrooid en maak meer code dan nodig is… vandaar, proberen.

dat gezegd hebbende, moeten we ook rekening houden met de taal – en omgevingsspecificaties in hoe we de assertion pass aanpakken-en de browser, met zijn geschiedenis, heeft veel te overwegen. Dat is geen kleinigheid, het is slechts een voorbeschouwing bij het stellen van verwachtingen ten aanzien van eisen.

specifiek: stel dat we zouden zeggen dat de stap kan worden geverifieerd met behulp van de Array.indexOf() methode op de collectie geretourneerd uit ‘ getAll ()’ op het object Grocery List? Zonder een polyfill, dan beperken we ons tot het doorgeven van beweringen op IE 9 en ouder. Dergelijke overwegingen zijn slechts het topje van de ijsberg bij de beslissing over wat te introduceren in uw codebase om uw tests passeren, en MOET echt worden overgelaten aan een team discussie over wat nodig wordt geacht om het product aan de productie.

ik zou verder kunnen gaan, maar laten we aannemen dat we alle bases willen behandelen als het gaat om browsers (dwz 6 en hoger, huiver). Naar mijn mening, om dit tweede Scenario groen te maken, zullen we een getItemIndex() methode toevoegen met de volgende handtekening:

+ getItemIndex(itemValue):int

we zullen eerst de stap wijzigen om te falen:

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

de aanvaarding om deze test te doorstaan is dat de index waar het toegevoegde item zich in de collectie bevindt NIET-negatief is. Voor dit scenario proberen we niet om een specificatie te valideren over waar een nieuw item wordt toegevoegd in een lijst (bijvoorbeeld, vooraf of toegevoegd), maar gewoon dat het toegankelijk is.

draaien dat een uitzondering zal opleveren:

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

laten we ons Object Grocery List wijzigen om de getItemIndex methode te ondersteunen:

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

bij onze implementatie van getItemIndex wordt de lijst doorlopen en, als item wordt gevonden, wordt de index geretourneerd. Anders wordt een waarde van -1 geretourneerd. In wezen, hoe de Array.indexOf methode werkt van ECMAScript 5.

opmerking: Ik weet dat het misschien dom lijkt om Object te gebruiken.maak van ECMAScript 5, maar niet Array.index van. De reden-meestal-is dat ik normaal altijd een polyfill voor Object.maken en niet voor Array.index van. Ik denk gewoonte.

nu als we de specs opnieuw uitvoeren onder Komkommerjs:

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

onze cukes zijn groen. (Dit is het punt waarop je je wenkbrauw veegt en langzaam klapt).

conclusie

In dit bericht introduceerde ik hoe ik de BDD tool CucumberJS gebruik om me te houden aan de Testgestuurde ontwikkeling in JavaScript. Ik ging door het gebruik van een voorbeeld van een enkele functie met twee scenario ‘ s en het draaien van falende stappen naar groene cukes. Als u niet bekend bent met het proces met het maken van tests falen eerst alleen om code te produceren om de test te passeren, Ik hoop dat je gevolgd langs; Ik kan langdradig zijn en het proces lijkt veel tijd in beslag te nemen, maar de ontwikkeling onder dergelijke praktijken begint soepel te verlopen zodra je in de groef komt. Bovendien, ik denk dat er een enorme beloning in het hebben van uw code onder een test harnas als het gaat om refactoring en bug fixing – zowel in de gezondheid van ontwikkelaars en het bedrijfsleven.

dit artikel is oorspronkelijk geplaatst op http://custardbelly.com/blog/blog-posts/2014/01/08/bdd-in-js-cucumberjs/index.html

figuur met dank aan http://commons.wikimedia.org/wiki/File:Og%C3%B3rki…jpg

e6b2cff6a55928c8f62b9d602add3fed?s = 100d = mmr = g

Todd Anderson is een applicatie ontwikkelaar met een passie voor architectuur en ontwikkeling workflows.

als een sterke voorstander van agile praktijken en Testgestuurde ontwikkeling met meer dan tien jaar ervaring, heeft hij geholpen web -, mobiele en desktop-oplossingen te leveren bij tal van bedrijven in de enterprise-en entertainmentindustrie, waaronder Adobe, THQ, Condé Nast Publications en Motorola.

hij schrijft regelmatig over software en technologie op zijn blog, support Open Source software en due my best to give back to the development community via GitHub en heeft het gewaardeerde genoegen gehad om coauteur te zijn op verschillende titels van O ‘ Reilly en Wiley Wrox: Amazon profile.

volg Todd op Twitter

bezoek Todd ‘ s site

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.