BDD v JavaScriptu s CucumberJS

0

Autor: Todd Anderson

dříve jsem psal o test driven development (TDD) v JavaScriptu, zejména pomocí knihovny stylu behavior driven development (BDD) Jasmine v sérii o budování testovací aplikace pro seznam potravin. V této sérii příspěvků jsem prošel přemýšlením uživatelských příběhů o funkcích a scénářích jako o skutečných vývojových úkolech a-čtení zpět – je to všechno velmi zelené (bez slovní hříčky), pokud hledám způsob, jak dodat testovaný kód. Na tom není nic špatného a s největší pravděpodobností se na tento a následující příspěvky podívám stejným způsobem. To znamená, že stále platí, že TDD je nejlepší způsob, jak dodat stručný, testovaný a promyšlený kód.

od té doby jsem však do svého pracovního postupu TDD pro projekty založené na JavaScriptu začlenil jiný nástroj, který mi umožňuje integraci specifikací funkcí blíže k mému vývoji a skutečně zahrnuje můj současný ideál vývoje řízeného chováním: CucumberJS. V podstatě mi to umožňuje skutečně dodržovat TDD při vývoji z vnějšku spuštěných automatizovaných testů, které selhávají, dokud nenapíšu kód, který podporuje funkci.

předpoklady a poznámky

pro příklady v tomto příspěvku se předpokládá, že jste obeznámeni s NodeJS, npm, vývojem modulů uzlů a běžnými postupy testování jednotek, protože tato témata jsou příliš velká na to, aby se v tomto příspěvku diskutovalo.

podpůrné soubory související s tímto tématem budou k dispozici na adrese:
https://github.com/bustardcelly/cucumberjs-examples

CucumberJS

CucumberJS je JavaScript port populárního nástroje BDD Cucumber (který sám byl přepsáním RSpec). To vám umožní definovat funkce SPECIFIKACE v doméně specifické-jazyk (DSL) – volal okurka-a spustit své SPECIFIKACE pomocí nástroje příkazového řádku, který bude hlásit předávání a / nebo selhání scénářů a kroky, které se skládají z.

je důležité si uvědomit, že okurka sama o sobě neposkytuje výchozí knihovnu tvrzení. Jedná se o testovací rámec poskytující nástroj příkazového řádku, který spotřebovává definované funkce a ověřuje scénáře spuštěním kroků napsaných v JavaScriptu. Je to volba vývojářů zahrnout požadovanou knihovnu tvrzení použitou k tomu, aby tyto kroky prošly nebo selhaly. Mým záměrem je objasnit proces příkladem prostřednictvím jedné funkce s více scénáři v tomto příspěvku.

instalace

můžete nainstalovat CucumberJS v projektu pomocí npm:

$ npm install cucumber --save-dev

okurka

pokud jste následovali v předchozí sérii TDD, najdete specifikace definované v této sérii podobné okurce. Ve skutečnosti budu znovu hash funkce spec z této série demonstrovat práci přes první cuke (aka, předávání funkce spec).

pokud bychom měli předělat aplikaci seznam potravin pod TDD / BDD pomocí okurek, nejprve bychom začali s funkcí pomocí syntaxe okurek:

/funkce/add-item.funkce

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

funkce definuje obchodní hodnotu, zatímco scénáře definují kroky, které tuto hodnotu poskytují. Nejčastěji ve světě vývoje softwaru jsou z těchto scénářů přijímány vývojové úkoly a definovány testy QA.

zastavil jsem se u dvou scénářů, ale mohli bychom velmi snadno přidat další scénáře k této funkci; okamžitě to, co přijde na mysl, jsou pravidla pro vkládání položek a ověření vlastností, které umožňují přidání nebo odmítnutí položky. Při zpětném pohledu by mohlo mít větší smysl vytvořit samostatné specifikace funkcí pro takové podrobnosti. Na taková témata bychom však mohli strávit celý příspěvek, takže se vraťme k již definované funkci.

v každém scénáři je seznam postupných kroků: daný, kdy a potom. Právě tyto kroky CucumberJS provede po spotřebování této funkce spec. Po každém z nich, můžete volitelně mít A A ale, nicméně – i když je to občas nutné a nevyhnutelné-snažím se držet dál od takových dalších krokových klauzulí.

spuštění

po uložení do souboru v adresáři a / features jej můžeme spustit pod okurkou:

$ node_modules/.bin/cucumber-js

ve výchozím nastavení CucumberJS spotřebuje všechny SPECIFIKACE funkcí nalezené v adresáři relativní / funkce.

aktuální výstup konzoly bude vypadat jako následující, což v podstatě znamená, že všechny kroky nebyly umístěny nebo definovány:

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

takže máme 6 nedefinovaných kroků, které tvoří 2 scénáře a nástroj CucumberJS ci dokonce poskytuje příklady, jak je definovat!

důležitou součástí tohoto úryvku je pochopit, že existují pouze 4 kroky k implementaci. V naší funkci máme 2 Scenerios každý s 3 kroky. Existuje celkem 6 kroků, ale stačí definovat 4. Důvodem je, že každý scénář sdílí stejný daný a kdy krok; tyto musí být definovány pouze jednou a budou spuštěny samostatně pro každý scénář. V podstatě, pokud definujete podobné kroky pomocí stejného kontextu, znovu použije „nastavení“ pro jeden krok v každém scénáři.

používám „setup“ v uvozovkách, protože to myslím spíše v roli definování kontextu pro Kdy a pak kroky.

nechci se zmást s metodami setup / teardown jiných postupů testování jednotek-které jsou známé jako Před / Po podpůrných úkolech v CucumberJS – a nesou více kontextu pro nastavení prostředí, ve kterém jsou testy prováděny (například vyplnění DB uživatelů) a poté stržení této sady.

definice kroků

v předchozí části jsme viděli, že spuštění CucumberJS proti naší funkci Přidat položku nás upozornilo, že máme nedefinované (a ačkoli nejsou vytištěny červeně, selhávají) scénáře pro podporu této funkce. Ve výchozím nastavení CucumberJS čte ve všech funkcích z adresáře / features vzhledem k místu, kde byl příkaz spuštěn, ale nemohl najít podporované krokové soubory, ve kterých jsou tyto metody definovány.

jak již bylo zmíněno, CucumberJS neposkytuje knihovnu tvrzení. Jediným předpokladem v tomto bodě-protože nástroj CucumberJS je spuštěn pod NodeJS-je, že podporované kroky budou načteny jako moduly uzlů s exportovanou funkcí, která má být provedena. Jakmile začneme tyto kroky implementovat, budeme se muset rozhodnout o knihovně tvrzení, kterou použijeme při ověřování naší logiky. V tuto chvíli to rozhodnutí položíme na polici a necháme nastavení Barebone selhat.

Chcete-li začít, Vezměme si tyto definice kroků poskytované nástrojem CucumberJS ci a vložte je do modulu uzlu:

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

ve výchozím nastavení bude CucumberJS hledat kroky, které mají být načteny do složky s názvem step_definitions v adresáři / features vzhledem k místu, kde příkaz vydáte. Volitelně můžete použít možnost -r k načtení kroků CucumberJS z jiného umístění. Spuštění výchozího nastavení je stejné jako nastavení následující volby adresáře definice kroku:

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

výstup konzoly bude nyní vypadat následovně:

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

není to příliš překvapivé, když oznamujeme zpětné volání stavu pending. CucumberJS vstoupí do prvního kroku (daný) a je okamžitě vrácen s čekajícím oznámením. Jako takový, neobtěžuje se zadáním dalších kroků a označí je jako přeskočené.

poznámka: je příliš mnoho na to, abychom se dostali do diskuse o modulech na straně klienta a AMD vs CommonJS. Pro účely tohoto příkladu budu používat CommonJS, protože mé současné zájmy spočívají v využití Browserify pro vývoj na straně klienta. Dlouho jsem byl zastáncem RequireJS a AMD. Znovu, to je úplně jiná diskuse.

vzhledem k tomu,

abychom se dostali blíže k zelené, budeme nejprve řešit daný krok:

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

kdybychom to znovu spustili, dostali bychom výjimku hned:

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

což je pochopitelné, protože jsme nedefinovali žádný jiný kód než tento modul definice kroku a snažíme se vyžadovat modul, který neexistuje. V lepení s TDD, to je dobrá věc – víme, proč to selhává, a očekáváme to – vytáhl bych si vlasy, kdyby to nevyhodilo výjimku!

abychom to mohli projít, vytvoříme modul uzlu v určeném adresáři, který exportuje objekt pomocí metody create:

/ script / model/grocery-list.js

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

poskytli jsme minimální požadavek, aby náš daný krok prošel. Budeme se starat o detaily, jak se blížíme k posledně uvedeným krokům.

spusťte to znovu a CucumberJS vstoupí do kroku kdy každého scénáře a přeruší se kvůli čekajícímu návratu.

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

když

v předchozí části jsme pro provedení daného kroku na každém scénáři implementovali počátky modelu seznamu potravin generovaného tovární metodou create z modulu grocery-list. Nechci se dostat do debaty o vytváření objektů, operátoru new, třídách a prototypech-alespoň ne v tomto příspěvku – a budu předpokládat, že jste obeznámeni a pohodlní (alespoň při čtení kódu) s objektem.vytvořit definováno pro ECMAScript 5.

při kontrole kroku kdy pro scénáře:

When I add an item to the list

…musíme poskytnout způsob, jak přidat položku do instance seznamu potravin vytvořené v daném-a to v co nejmenším kódu, aby krok prošel.

nejprve definujeme naše očekávání make-upu a add podpisu seznamu potravin v definicích kroků:

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

pokud to znovu spustíme:

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

Oooo-Wee! Teď mluvíme. Velké, jasně červené F.

aby se to vrátilo k předávání, upravíme grocery-list s co nejmenším kódem:

/ script / model/grocery-list.js

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

spusťte znovu a CucumberJS pokročil do kroků, které hlásí stav pending.

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

pak

postupovali jsme prostřednictvím našich krokových implementací a dosáhli jsme kroku(kroků), při kterém uplatňujeme operace a vlastnosti, které dokazují, že náš scénář poskytuje zamýšlenou hodnotu. Jak již bylo zmíněno, CucumberJS neposkytuje knihovnu tvrzení. Moje preference v knihovnách assertion je kombinace Chai, Sinon a Sinon-Chai, ale pro příklady v tomto příspěvku použiji modul assert, který je dodáván s NodeJS. Doporučuji vám podívat se na další knihovny tvrzení a zanechat poznámku, pokud máte oblíbenou.

Poznámka: Tato část bude malým příkladem, protože rychle přecházíme z úpravy našeho kódu a často spouštíme spec runner.

první scénář

při kontrole prvního scénáře pak krok:

Then The grocery list contains a single item

…budeme muset prokázat, že instance seznamu potravin roste o faktor 1 pro každou novou přidanou položku.

Aktualizujte krok a definujte, jak očekáváme, že specifikace bude ověřena:

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

zatáhli jsme modul assert a pokusili jsme se ověřit, že délka seznamu potravin vzrostla o hodnotu 1 Po spuštění předchozího kroku při přidání položky.

spusťte to a dostaneme výjimku:

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

přidejte tuto metodu do našeho modelu seznamu potravin:

/ script / model/grocery-list.js

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

a zpět na spuštění našich specifikací:

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

vzhledem k tomu, že kód nevrací nic z getAll(), nemůžeme získat přístup k vlastnosti length pro náš test tvrzení.

pokud upravíme kód tak, aby vrátil pole:

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

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

a znovu spusťte SPECIFIKACE, dostaneme chybovou zprávu o tvrzení, kterou jsme poskytli:

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

nyní máme řádné selhání, které nám bylo hlášeno z tvrzení, které způsobuje, že krok neprošel. Hurá!

Udělejte si oddech

pojďme se na chvíli pozastavit, než přidáme další kód, aby tento krok prošel. Problém není ve skutečnosti přidání položky do pole, které se vrací, jde spíše o zajištění toho, aby byla položka přidána pomocí metody add a výsledek z getAll je seznam rozšířený o tuto položku.

podrobnosti o implementaci, které se podílejí na provádění tohoto testu, jsou tam, kde váš tým využívá své zkušenosti s architekturou, ale je třeba dbát na to, aby byl přidán pouze nejdůležitější kód. Měli byste se vyhnout přes palubu při přemýšlení o vnitřcích modelu kolekce seznam potravin. Je to kluzké těsné lano, které by mohlo snadno spadnout do králičí nory-stejně jako ta špatně formulovaná metafora 🙂

vraťte se do práce!

pro účely tohoto příkladu použijeme argument propertiesObject Object.create k definování list getteru, který bude sloužit jako proměnlivé pole pro naše položky seznamu potravin:

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

pokud to spustíme, zjistíme, že první scénář nyní prochází!

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

druhý scénář

při kontrole posledního kroku našeho 2. scénáře přistupuje čekající implementace K přidané položce:

Then I can access that item from the grocery list

Chcete-li tento krok projít, musíme ověřit, že můžeme přistupovat k položce připojené k seznamu potravin vyvoláním položky add().

stejně jako při implementaci přístupu k délce seznamu potravin existuje několik způsobů, jak bychom mohli tento test projít v kódu. Opět mám pocit, že to je místo, kde zkušenosti s vývojem softwaru a chuť přichází do hry s ohledem na architekturu, ale také se raději snažím produkovat co nejmenší množství kódu; a já budu první, kdo připustí, že někdy jdu trochu roztržitý a vytvořím více kódu, než je nutné … proto se snažím.

to znamená, že musíme také vzít v úvahu specifikace jazyka a prostředí při řešení průchodu tvrzení – a prohlížeč s jeho historií má mnoho, co je třeba zvážit. To není nepatrné, je to jen předvídavost při stanovování očekávání požadavků.

konkrétně: předpokládejme, že tento krok lze ověřit pomocí metody Array.indexOf() na kolekci vrácené z „getAll ()“ v objektu seznam potravin? Bez polyfill, pak se omezujeme na předávání tvrzení na IE 9 a starší. Takové úvahy jsou jen špičkou ledovce při rozhodování o tom, co zavést do své kódové základny, aby vaše testy prošly, a opravdu by mělo být ponecháno na týmové diskusi o tom, co je považováno za nezbytné pro získání produktu do výroby.

mohl bych pokračovat dál a dál, ale předpokládejme, že chceme pokrýt všechny základny, pokud jde o prohlížeče (tj. Podle mého názoru, aby se tento druhý scénář změnil na zelenou, přidáme metodu getItemIndex() s následujícím podpisem:

+ getItemIndex(itemValue):int

nejprve upravíme krok, aby selhal:

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

akceptací tohoto testu je, že index, ve kterém se přidaná položka nachází ve sbírce, není negativní. Pro tento scénář se nesnažíme ověřit specifikaci, kde je nová položka přidána do seznamu (např.

běh, který vytvoří výjimku:

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

upravme náš objekt seznamu potravin tak, aby podporoval metodu 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 } }); }};

v naší implementaci getItemIndex je seznam procházen a pokud je položka nalezena, index je vrácen. V opačném případě se vrátí hodnota -1. V podstatě, jak metoda Array.indexOf funguje v ECMAScript 5.

Poznámka: vím, že se může zdát hloupé používat objekt.vytvořit z ECMAScript 5, ale ne pole.indexOf. Důvodem-většinou-je to, že obvykle vždy zahrnuji polyfill pro objekt.vytvořit a ne pro pole.indexOf. Asi zvyk.

nyní, pokud znovu spustíme SPECIFIKACE pod CucumberJS:

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

naše cukety jsou zelené! (To je bod, který si otřete obočí a pomalu tleskáte).

závěr

v tomto příspěvku jsem představil, jak používám nástroj BDD CucumberJS, abych dodržel Test řízený vývoj v JavaScriptu. Prošel jsem pomocí příkladu jedné funkce se dvěma scénáři a otočením neúspěšných kroků k green cukes. Pokud nejste obeznámeni s procesem s tím, že testy selžou nejprve pouze při výrobě kódu pro provedení testu, doufám, že jste následovali; Možná jsem rozvláčný a proces by se mohl zdát, že trvá hodně času, ale vývoj v rámci takových postupů se začne hladce pohybovat, jakmile se dostanete do drážky. Navíc si myslím, že je obrovská odměna za to, že váš kód je pod testovacím postrojem, pokud jde o refaktorování a opravu chyb-jak ve zdraví vývojářů, tak v podnikání.

tento článek byl původně zaslán na http://custardbelly.com/blog/blog-posts/2014/01/08/bdd-in-js-cucumberjs/index.html

obrázek se svolením http://commons.wikimedia.org/wiki/File:Og%C3%B3rki…jpg

e6b2cff6a55928c8f62b9d602add3fed?s=100 D=mmr=g

Todd Anderson je vývojář aplikací s vášní pro architekturu a vývojové pracovní postupy.

jako silný zastánce agilních postupů a vývoj řízený testem s více než desetiletou zkušeností pomohl dodávat webová, mobilní a desktopová řešení s mnoha společnostmi v podnikovém a zábavním průmyslu, včetně Adobe, THQ, Condé Nast Publications a Motorola.

často píše o softwaru a technologii na svém blogu, podporuje software s otevřeným zdrojovým kódem a díky tomu, co je v mých silách, abych se vrátil vývojové komunitě prostřednictvím GitHubu a měl vážené potěšení být spoluautorem několika titulů z profilu O ‚ Reilly a Wiley Wrox: Amazon.

Sledujte Todda na Twitteru

navštivte stránky Todda

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.