BDD în JavaScript cu CucumberJS

0

de Todd Anderson

am scris anterior despre test driven development (TDD) în JavaScript, mai ales folosind biblioteca de stil behavior driven development (BDD) Jasmine într-o serie despre construirea unei aplicații de listă alimentară bazată pe teste. În acea serie de postări am trecut prin gândirea poveștilor utilizatorilor pentru funcții și scenarii ca sarcini reale de dezvoltare și – citind din nou – totul este foarte verde (fără joc de cuvinte intenționat) în măsura în care găsesc o modalitate de a livra Cod condus de test. Nu este nimic în neregulă cu asta și cel mai probabil voi privi acest lucru și postările ulterioare în același mod. Acestea fiind spuse, încă mai dețin adevărat că TDD este cel mai bun mod de a oferi un cod concis, testat și bine gândit.

din acel moment, cu toate acestea, am încorporat un instrument diferit în fluxul meu de lucru TDD pentru proiecte bazate pe JavaScript, care îmi permite integrarea specificațiilor de caracteristici mai îndeaproape cu dezvoltarea mea și cuprinde cu adevărat idealul meu actual de dezvoltare bazată pe comportament: CucumberJS. În esență, îmi permite să adere cu adevărat la TDD în timp ce în curs de dezvoltare din exterior în curs de desfășurare teste automate care nu reușesc până când am scris cod care acceptă o caracteristică.

ipoteze și note

pentru exemplele din acest post, se presupune că sunteți familiarizați cu NodeJS, npm, în curs de dezvoltare Module de nod și practici comune de testare unitate ca aceste subiecte prea mari pentru a discuta în acest post.

fișierele suport legate de acest subiect vor fi disponibile la:
https://github.com/bustardcelly/cucumberjs-examples

CucumberJS

CucumberJS este un port JavaScript al popularului instrument BDD castravete (care în sine a fost o rescriere a RSpec). Vă permite să definiți specificațiile de caracteristici într-un limbaj specific domeniului (DSL)-numit Gherkin – și să rulați specificațiile utilizând un instrument de linie de comandă care va raporta trecerea și/sau eșecul scenariilor și pașii din care sunt alcătuite.

este important să rețineți că castravetele în sine nu oferă o bibliotecă de afirmații implicită. Este un cadru de testare care oferă un instrument de linie de comandă care consumă Caracteristici definite și validează scenarii prin rularea pașilor care sunt scrise în JavaScript. Este alegerea dezvoltatorilor să includă biblioteca de afirmații dorită utilizată pentru a face ca acești pași să treacă sau să eșueze. Intenția mea este să clarific procesul prin exemplu printr-o singură caracteristică cu mai multe scenarii în acest post.

instalare

puteți instala CucumberJS în proiectul dvs. folosind npm:

$ npm install cucumber --save-dev

Gherkin

dacă ați fi urmat în seria TDD anterioară, veți găsi specificațiile definite în acea serie similare cu Gherkin. De fapt, voi re-hashing un spec caracteristică din acea serie pentru a demonstra de lucru prin prima ta Cuke (aka, care trece spec caracteristică).

dacă ar fi să refacem aplicația Listă de cumpărături sub TDD/BDD folosind castravete, am începe mai întâi cu o caracteristică folosind sintaxa Gherkin:

/caracteristici/add-item.caracteristică

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

caracteristica definește o valoare de afaceri, în timp ce scenariile definesc pașii care furnizează acea valoare. Cel mai adesea, în lumea dezvoltării de software, din aceste scenarii sunt preluate sarcinile de dezvoltare și sunt definite testele QA.

m-am oprit la două scenarii, dar am putea adăuga foarte ușor mai multe scenarii la această caracteristică; imediat ce îmi vine în minte sunt regulile de inserare a elementelor și validarea proprietăților care permit adăugarea sau respingerea unui element. În retrospectivă, ar putea avea mai mult sens să creați specificații separate pentru astfel de detalii. Totuși, am putea petrece o postare întreagă pe astfel de subiecte, așa că să revenim la funcția deja definită.

în cadrul fiecărui scenariu este o listă de pași secvențiali: dat, când și apoi. Este acești pași care CucumberJS va executa după ce a consumat această caracteristică spec. După fiecare dintre acestea, puteți avea opțional și dar, totuși – deși uneori este necesar și inevitabil – încerc să stau departe de astfel de clauze suplimentare de pas.

rularea acestuia

după ce am salvat acest lucru într-un fișier din directorul a /features, îl putem rula sub castravete:

$ node_modules/.bin/cucumber-js

în mod implicit, CucumberJS va consuma toate specificațiile de caracteristici găsite în directorul relativ /caracteristici.

ieșirea curentă a consolei va arăta ceva de genul următor, ceea ce înseamnă, în esență, că toți pașii nu au fost localizați sau definiți:

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

deci avem 6 pași nedefiniți care alcătuiesc 2 scenarii, iar instrumentul CucumberJS ci oferă chiar exemple pentru modul de definire a acestora!

o parte importantă a acelui fragment de înțeles este că există doar 4 pași de implementat. În caracteristica noastră avem 2 Scenerios fiecare cu 3 pași. Există un total de 6 pași, dar trebuie doar să definim 4. Motivul este că fiecare scenariu împărtășește același pas dat și când; acestea trebuie definite o singură dată și vor fi rulate separat pentru fiecare scenariu. În esență, dacă definiți pași similari folosind același context, acesta va reutiliza „configurarea” pentru un singur pas în cadrul fiecărui scenariu.

folosesc „setup” în ghilimele pentru că mă refer mai mult la rolul de a defini contextul pentru când și apoi pașii.

nu vreau să – l confundat cu metodele de configurare/teardown de alte practici de testare unitate – care sunt cunoscute ca înainte/după sarcini de sprijin în CucumberJS-și transporta mai mult de un context pentru crearea unui mediu în care testele sunt apoi executate (cum ar fi umplerea unui DB de utilizatori) și apoi ruperea în jos că înființat.

definiții pas

în secțiunea anterioară, am văzut că rulează CucumberJS împotriva caracteristica noastră Add element ne-a alertat că am nedefinit (și, deși nu imprimate în roșu, în lipsa) scenarii pentru a sprijini caracteristica. În mod implicit CucumberJS citește în toate caracteristicile din directorul /caracteristici în raport cu locul în care a fost executată comanda, dar nu a putut localiza fișierele pas acceptate în care sunt definite aceste metode.

după cum am menționat anterior, CucumberJS nu oferă o bibliotecă de afirmații. Singura presupunere în acest moment – deoarece instrumentul CucumberJS este rulat sub NodeJS-este că pașii suportați vor fi încărcați ca module de noduri cu o funcție exportată care urmează să fie executată. Pe măsură ce începem să implementăm pașii, va trebui să decidem biblioteca de afirmații pe care să o folosim în validarea logicii noastre. Vom pune această decizie pe raft în acest moment și vom face ca configurarea barebones să eșueze.

pentru a începe, să luăm aceste definiții pas furnizate de instrumentul CucumberJS ci și fixați-le într-un modul nod:

/caracteristici/stepdefinitions/add-item.pași.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(); });};

în mod implicit, CucumberJS va căuta pașii care trebuie încărcați într-un folder intitulat step_definitions în directorul /features în raport cu locul în care emiteți comanda. Puteți utiliza opțional opțiunea -r pentru a avea pași de încărcare CucumberJS dintr-o altă locație. Rularea implicită este aceeași cu setarea următorului pas definiție director opțiune:

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

ieșirea consolei va arăta acum după cum urmează:

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

nu prea surprinzător văzând cum anunțăm apelul invers al unui stat pending. CucumberJS intră în primul pas (dat) și este returnat imediat cu o notificare în așteptare. Ca atare, nu se deranjează cu introducerea oricăror pași ulteriori și le marchează ca omise.

Notă: este prea mult pentru a intra într-o discuție despre modulele client-side și AMD vs CommonJS. În scopul acestui exemplu, voi folosi CommonJS, deoarece interesele mele actuale constau în utilizarea Browserify pentru dezvoltarea clientului. Pentru o lungă perioadă de timp, am fost un susținător al RequireJS și AMD. Din nou, aceasta este o cu totul altă discuție.

dat

pentru a ne apropia de verde, vom aborda mai întâi pasul dat:

/caracteristici/definiții stepdefinitions/add-item.pas.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(); }); ...};

dacă am rula din nou, am obține o excepție imediat:

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

ceea ce este de înțeles, deoarece nu am definit niciun alt cod decât acest modul de definire a pasului și încercăm să solicităm un modul care nu există. În lipirea cu TDD, acesta este un lucru bun – știm de ce eșuează și ne așteptăm – mi-aș trage părul dacă nu ar arunca o excepție!

pentru a trece acest lucru, vom crea un modul nod în directorul specificat Thath exportă un obiect cu o metodă create:

/script/model/băcănie-list.js

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

am oferit cerința minimă pentru a trece pasul nostru dat. Ne vom îngrijora de detalii pe măsură ce ne apropiem de pașii din urmă.

rulați-l din nou, iar CucumberJS intră în etapa când a fiecărui scenariu și se întrerupe din cauza revenirii în așteptare.

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

când

în secțiunea anterioară, pentru a face pasul dat să treacă pe fiecare scenariu, am implementat începuturile unui model de listă de cumpărături generat dintr-o metodă din fabrică, create, din modulul grocery-list. Nu vreau să intru într – o dezbatere despre crearea obiectelor, operatorul new, clasele și prototipurile – cel puțin nu în acest post-și veți presupune că sunteți familiarizați și confortabili (cel puțin în citirea codului) cu obiectul.creați definit pentru ECMAScript 5.

în revizuirea pas atunci când pentru scenarii:

When I add an item to the list

…trebuie să oferim o modalitate prin care să adăugăm un element la instanța de listă alimentară creată în dat – și să facem acest lucru în cât mai puțin cod pentru a face pasul să treacă.

în primul rând, vom defini așteptările noastre de make up și add semnătura listei alimentare în definițiile pas:

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

dacă vom rula din nou:

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

oooo-DEEE! Acum vorbim. Mare, rosu aprins f ‘ s.

pentru a face ca ajunge înapoi la trecerea, vom modifica grocery-list cu cod cât mai puțin posibil:

/script/model/băcănie-listă.js

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

rulați din nou, iar CucumberJS a progresat la pașii de atunci care raportează o stare pending.

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

apoi

am progresat prin implementările pasului nostru și am ajuns la Pasul(etapele) la care afirmăm operațiuni și proprietăți care dovedesc că scenariul nostru oferă valoarea dorită. Așa cum am menționat anterior, CucumberJS nu oferă o bibliotecă de afirmații. Preferința mea în bibliotecile de afirmații este o combinație de Chai, Sinon și Sinon-Chai, dar pentru exemplele din acest post, voi folosi modulul assert care vine cu NodeJS. Vă încurajez să verificați alte biblioteci de afirmații și să lăsați o notă dacă aveți un favorit.

Notă: Această secțiune va fi un mic exemplu greu, deoarece trecem rapid de la modificarea Codului nostru și rulăm frecvent alergătorul spec.

primul scenariu

în revizuirea pas apoi primul scenariu:

Then The grocery list contains a single item

…va trebui să dovedim că instanța listei alimentare crește cu un factor de 1 pentru fiecare articol nou adăugat.

actualizați pasul pentru a defini modul în care ne așteptăm ca specificația să fie validată:

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

am introdus modulul assert și am încercat să validăm că lungimea listei de Cumpărături a crescut cu o valoare de 1 după ce am rulat pasul anterior când am adăugat elementul.

rulați că și vom obține o excepție:

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

să adăugăm această metodă la modelul nostru de listă alimentară:

/script/model/băcănie-listă.js

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

și înapoi la rularea specificațiilor noastre:

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

având în vedere că codul nu returnează nimic de la getAll(), nu putem accesa o proprietate length pentru testul nostru de afirmare.

dacă modificăm codul pentru a returna o matrice:

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

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

și rulați din nou specificațiile, vom primi mesajul de eroare de afirmație pe care l-am furnizat:

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

acum, avem un eșec propriu-zis care ne este raportat dintr-o afirmație care face ca Pasul să nu treacă. Ura!

ia o pauză

să pauză aici pentru o secundă înainte de a adăuga mai mult cod pentru a obține acest pas pentru a trece. Problema la îndemână nu este de fapt adăugarea unui element la matrice fiind returnate, este mai mult despre asigurarea că un element este adăugat prin add metoda și rezultatul din getAll fiind o listă extinsă cu acel element.

detaliile de implementare care sunt implicate în realizarea acestui test sunt locul în care echipa dvs. își folosește experiența de arhitectură, dar este necesar să se adauge doar cel mai esențial Cod. Ar trebui să evite merge peste bord în gândire despre interne ale modelului de colectare Lista de cumparaturi. Este o frânghie alunecoasă, care ar putea cădea cu ușurință într – o gaură de iepure-la fel ca acea metaforă prost formulată,

întoarce-te la muncă!

în scopul acestui exemplu, vom folosi argumentul propertiesObject al Object.create pentru a defini un getter list care va servi ca o matrice mutabilă pentru elementele noastre din lista de cumpărături:

/script/model/băcănie-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 } }); }};

dacă vom rula că, vom găsi că primul scenariu este acum trece!

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

al doilea scenariu

în revizuirea ultimului pas al scenariului nostru 2nd, implementarea în așteptare accesează elementul adăugat:

Then I can access that item from the grocery list

pentru a trece acest pas, trebuie să verificăm dacă putem accesa articolul anexat la lista de produse alimentare invocând add() cu un articol.

ca și în cazul implementării accesării lungimii listei alimentare, există mai multe moduri în care am putea face acest test să treacă în cod. Din nou, simt că aici intră în joc experiența și gustul dezvoltării de software în ceea ce privește arhitectura, dar prefer, de asemenea, să încerc să produc cea mai mică cantitate de cod posibil; și voi fi primul care va admite că uneori merg puțin absent și creez mai mult cod decât este necesar… prin urmare, încercând.

acestea fiind spuse, trebuie să luăm în considerare și specificațiile de limbă și mediu în modul în care abordăm trecerea afirmației – iar browserul, cu istoricul său, are multe de luat în considerare. Aceasta nu este o ușoară, este doar o preconcepție în stabilirea așteptărilor pentru cerințe.

mai exact: să presupunem că am spune că Pasul poate fi verificat folosind metoda Array.indexOf() din colecția returnată de la ‘getAll ()’ pe obiectul listei de cumpărături? Fără o polyfill, atunci ne limităm la transmiterea afirmațiilor pe IE 9 și mai în vârstă. Astfel de considerații sunt doar vârful aisbergului atunci când decideți ce să introduceți în baza de cod pentru ca testele dvs. să treacă și ar trebui să fie lăsate la o discuție în echipă cu privire la ceea ce este considerat necesar pentru a obține produsul la producție.

aș putea continua, dar să presupunem că vrem să acoperim toate bazele atunci când vine vorba de browsere (adică 6 și mai sus, tremur). În opinia mea, pentru a face acest al doilea scenariu să devină verde, vom adăuga o metodă getItemIndex() cu următoarea semnătură:

+ getItemIndex(itemValue):int

mai întâi vom modifica pasul pentru a eșua:

/caracteristică/stepdefinitions/add-item.pas.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();});

acceptarea pentru ca acest test să treacă este că indexul la care se află elementul adăugat în colecție nu este negativ. Pentru acest scenariu, nu încercăm să validăm o specificație cu privire la locul în care un element nou este adăugat într-o listă (de exemplu, prepended sau addended), ci pur și simplu că este accesibil.

rularea care va produce o excepție:

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

să modificăm Obiectul nostru de listă alimentară pentru a sprijini metoda getItemIndex:

/script/model/băcănie-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 } }); }};

în implementarea noastră a getItemIndex, lista este traversată și, dacă se găsește elementul, indexul este returnat. În caz contrar, se returnează o valoare de -1. În esență, Cum funcționează metoda Array.indexOf a ECMAScript 5.

notă: știu că poate părea o prostie să folosească obiect.creați din ECMAScript 5, dar nu matrice.indexOf. Motivul-mai ales-fiind că în mod normal includ întotdeauna un polyfill pentru obiect.creați și nu pentru matrice.indexOf. Presupun că obiceiul.

acum, dacă vom rula specificatiile din nou sub CucumberJS:

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

cukes noastre sunt verzi! (Acesta este punctul în care vă ștergeți fruntea și bateți încet).

concluzie

în acest post, am introdus modul în care folosesc instrumentul BDD CucumberJS pentru a adera la dezvoltarea bazată pe teste în JavaScript. Am trecut prin utilizarea unui exemplu de o singură caracteristică cu două scenarii și de cotitură în lipsa pași pentru cukes verzi. Dacă nu sunteți familiarizați cu procesul cu efectuarea testelor nu reușesc mai întâi doar pentru a produce cod pentru a face testul să treacă, sper că ați urmat; S-ar putea să fiu prolix și procesul ar putea părea să dureze mult timp, dar dezvoltarea în astfel de practici începe să se miște fără probleme odată ce intri în canelură. În plus, cred că există o recompensă uriașă în a avea codul dvs. sub un ham de testare atunci când vine vorba de refactorizare și remedierea erorilor – atât în sănătatea dezvoltatorilor, cât și în afaceri.

acest articol a fost postat inițial la http://custardbelly.com/blog/blog-posts/2014/01/08/bdd-in-js-cucumberjs/index.html

pentru imagine, multumim http://commons.wikimedia.org/wiki/File:Og%C3%B3rki…jpg

e6b2cff6a55928c8f62b9d602add3fed?s = 100D = mmr = g

Todd Anderson este un dezvoltator de aplicații cu o pasiune pentru fluxurile de lucru pentru arhitectură și dezvoltare.

ca un susținător puternic al practicilor agile și al dezvoltării bazate pe teste, cu peste un deceniu de experiență, el a ajutat la furnizarea de soluții web, mobile și desktop cu numeroase companii din industria întreprinderilor și a divertismentului, inclusiv Adobe, THQ, Cond7 Nast Publications și Motorola.

el scrie frecvent despre software și tehnologie pe blogul său, susține software-ul Open Source și datorită celor mai bune mele pentru a da înapoi comunității de dezvoltare prin GitHub și au avut plăcerea stimat de a fi un co-autor pe mai multe titluri de la O ‘ Reilly și Wiley Wrox: Amazon profil.

urmați Todd pe Twitter

vizitați site-ul lui Todd

Lasă un răspuns

Adresa ta de email nu va fi publicată.