BDD em JavaScript com CucumberJS

0

Por Todd Anderson

eu já havia escrito sobre o desenvolvimento orientado a testes (TDD) em JavaScript, nomeadamente usando o behavior driven development (BDD) library, de estilo do Jasmim em uma série sobre a criação de um Test-Driven Lista de Supermercado Aplicação. Nessa série de posts eu passei a pensar em histórias de usuários para características e cenários como tarefas de desenvolvimento real, e – lendo de volta sobre ele – é tudo muito verde (sem trocadilho pretendido), na medida em que eu estou encontrando uma maneira de entregar o código de teste-driven. Não há nada de errado com isso e, muito provavelmente, irei analisar este e os postos subsequentes da mesma forma. Dito isto, continuo a afirmar que a TDD é a melhor forma de fornecer um código conciso, testado e bem pensado.

desde então, no entanto, eu incorporei uma ferramenta diferente no meu fluxo de trabalho TDD para projetos baseados em JavaScript que me oferece a integração de especificações de recursos mais próximo ao meu desenvolvimento e realmente engloba o meu atual ideal de Desenvolvimento Orientado por comportamento: pepinos. Essencialmente, permite – me aderir verdadeiramente ao TDD enquanto desenvolve a partir do exterior em execução testes automatizados que falham até que eu tenha escrito um código que suporta uma característica.

Pressupostos e Notas

Para os exemplos deste post, presume-se que você está familiarizado com NodeJS, a ngp, o desenvolvimento de Nó de módulos comuns e os testes de unidade de práticas de como estes temas demasiado grandes para discutir neste post.Os ficheiros de apoio relacionados com este tópico estarão disponíveis em::
https://github.com/bustardcelly/cucumberjs-examples

CucumberJS

CucumberJS é uma porta JavaScript do popular pepino de Ferramenta BDD (que por si só era uma reescrita do RSpec). Ele permite definir especificações de recursos em um domínio específico-linguagem (DSL) – chamado Gherkin-e executar suas especificações usando uma ferramenta de linha de comando que irá relatar a passagem e/ou falha de cenários e os passos que eles são compostos.

é importante notar que o próprio pepino não fornece uma biblioteca de asserções padrão. É um framework de testes que fornece uma ferramenta de linha de comando que consome recursos definidos e valida cenários, executando passos que são escritos em JavaScript. É a escolha dos desenvolvedores para incluir a biblioteca de asserção desejada usada para fazer esses passos passar ou falhar. É minha intenção esclarecer o processo por exemplo através de um único recurso com vários cenários neste post.

instalação

pode instalar pepinos no seu projecto usando o npm:

$ npm install cucumber --save-dev

Gherkin

se tivesse seguido a série TDD anterior, encontrará as especificações definidas nessa série semelhantes às do Gherkin. Na verdade, eu vou re-hashing um recurso spec dessa série para demonstrar o trabalho através de seu primeiro cuke (aka, passing feature spec).

se fôssemos refazer a aplicação da lista de compras em TDD / BDD usando pepino, começaríamos com uma característica usando a sintaxe de Gherkin:

/ features / add-item.característica

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

a característica define um valor de negócio, enquanto os cenários definem os passos que fornecem esse valor. Na maioria das vezes, no mundo do desenvolvimento de software, é a partir desses cenários que as tarefas de desenvolvimento são tomadas e testes de QA são definidos.

parei em dois cenários, mas poderíamos muito facilmente adicionar mais cenários a esta funcionalidade; imediatamente o que vem à mente são regras de inserção de itens e validação de propriedades que permitem que um item seja adicionado ou rejeitado. Em retrospectiva, poderia fazer mais sentido criar especificações de recursos separados para tais detalhes. Poderíamos gastar um post inteiro em tais tópicos, então vamos voltar ao recurso já definido.

dentro de cada cenário existe uma lista de etapas sequenciais: dadas, quando e então. São estes passos que o CucumberJS executará depois de ter consumido este recurso spec. Depois de cada um deles, você pode opcionalmente ter e E, Mas, no entanto – embora necessário e inevitável às vezes – eu tento ficar longe de tais cláusulas de passos adicionais.Depois de ter guardado isso para um ficheiro no directório A / features, podemos então executá – lo sob o nome de Cucumber.:

$ node_modules/.bin/cucumber-js

por padrão, o CucumberJS irá consumir todas as especificações de recursos encontradas no diretório relativo /recursos.

a saída actual da consola será semelhante ao seguinte, o que significa essencialmente que todos os passos não foram localizados ou definidos:

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

assim, temos 6 etapas indefinidas que compõem 2 cenários e a ferramenta CucumberJS ci até fornece exemplos de como defini-los!

uma parte importante desse trecho para entender é que existem apenas 4 passos para implementar. No nosso recurso temos 2 Scenerios cada um com 3 passos. Há um total de 6 passos, mas só precisamos definir 4. A razão é que cada cenário compartilha o mesmo dado e quando passo; estes só precisam ser definidos uma vez e serão executados separadamente para cada cenário. Essencialmente, se você definir passos semelhantes usando o mesmo contexto, ele vai reutilizar a “configuração” para um único passo dentro de cada cenário.

eu uso “configuração” entre aspas porque eu quero dizer mais no papel de definir o contexto para quando e depois passos.

eu não quero o confunda com o programa de configuração/subdivisão de métodos de outros testes de unidade de práticas – que são conhecidos como Antes/Após as tarefas de suporte em CucumberJS – e levar mais de um contexto para a configuração de um ambiente em que os testes são executados (como o preenchimento de um DB de usuários) e, em seguida, derrubando que configurar.

Step Definitions

In the previous section, we saw that running CucumberJS against our Add Item Feature alerted us that we have undefined (and, though not printed in red, failing) scenarios to support the feature. Por padrão, o CucumberJS lê em todas as funcionalidades do directório /features em relação ao local onde o comando foi executado, mas não conseguiu localizar os ficheiros step suportados em que estes métodos são definidos.

como mencionado anteriormente, CucumberJS não fornece uma biblioteca de asserções. A única suposição neste ponto-uma vez que a ferramenta CucumberJS é executada sob NodeJS – é que os passos suportados serão carregados como módulos de nó com uma função exportada a ser executada. À medida que começamos a implementar os passos, teremos de decidir sobre a biblioteca de asserções a usar para validar a nossa lógica. Vamos pôr essa decisão na prateleira de momento e fazer com que os barebones falhem.

para começar, vamos tomar as definições de passos fornecidas pela ferramenta de ci do CucumberJS e jogá-las em um módulo de nó:

/características/definições de etapas/add-item.passo.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(); });};

por omissão, o CucumberJS irá procurar por passos a serem carregados dentro de uma pasta chamada step_definitions na pasta /features em relação ao local onde emite o comando. Você pode opcionalmente usar a opção -r para ter passos de carga de pepinos de outro local. Executar a predefinição é o mesmo que definir a seguinte opção de definição de passo directório:

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

a saída da consola será agora parecida com a seguinte:

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

não é muito surpreendente, visto que notificamos o retorno de um estado pending. O pepino entra no primeiro passo (dado) e é imediatamente devolvido com uma notificação pendente. Como tal, não se incomoda em entrar em quaisquer passos subsequentes e marca-los como ignorados.

nota: é demasiado para entrar numa discussão sobre módulos do lado do cliente e AMD vs CommonJS. Para os propósitos deste exemplo eu estarei usando CommonJS, como eu meus interesses atuais estão na utilização de Browserify para o desenvolvimento do lado do cliente. Durante muito tempo, fui um defensor dos Requejs e da AMD. Mais uma vez, esta é uma outra discussão.

dado

para nos aproximarmos do verde, abordaremos primeiro o passo dado:

/ características/definições de etapas / add-item.passo.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(); }); ...};

:

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

o que é compreensível, uma vez que não definimos nenhum outro código, mas este módulo de definição de passo e estamos tentando exigir um módulo que não existe. Ao manter o TDD, isto é uma coisa boa – sabemos porque está a falhar e esperamos – eu estaria a arrancar o meu cabelo se não abrisse uma excepção!

a fim de fazer isto passar, vamos criar um módulo de nó no diretório especificado que exporta um objeto com um método create:

/script/model / Mercearia-list.js

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

nós fornecemos a exigência mínima para obter o nosso passo dado para passar. Preocupamo-nos com os detalhes à medida que nos aproximamos dos últimos passos.

execute isso novamente, e os pepinos entram na etapa de quando de cada cenário e abortam devido ao retorno pendente.

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

quando

na seção anterior, para fazer o passo dado em cada cenário implementamos o início de um modelo de Lista de compras gerado a partir de um método de fábrica, create, a partir do módulo grocery-list. Eu não quero entrar em um debate sobre a criação de objetos, o operador new, classes e protótipos – pelo menos não neste post – e vai assumir que você está familiarizado e confortável (pelo menos em leitura de código) com objeto.criar definido para o ECMAScript 5.

na revisão da etapa de quando para os cenários:

When I add an item to the list

…precisamos fornecer uma forma de adicionar um item à instância da lista de compras criada no dado-e fazê-lo em um pequeno código para fazer o passo passar.

Primeiro, vamos definir a nossa expectativa da composição e add assinatura da lista de compras nas definições de passos:

/características/definições de etapas/add-item.passo.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(); }); ...};

se repetirmos:

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

Ooo-Wee! Assim é que é falar. Big, bright red F’s. 🙂

para fazer isso voltar a passar, vamos modificar grocery-list com o menor Código possível:

/script / model / Mercearia-list.js

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

execute novamente, e CucumberJS progrediu para os então passos que estão relatando um estado pending.

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

depois

progredimos através das nossas implementações passo a passo e chegamos ao(S) passo (s) em que afirmamos operações e propriedades que provam que o nosso cenário fornece o seu valor pretendido. Como mencionado anteriormente, o CucumberJS não fornece uma biblioteca de asserções. Minha preferência em bibliotecas de asserção é uma combinação de Chai, Sinon e Sinon-Chai, mas para os exemplos neste post, eu vou apenas usar o módulo assert que vem com NodeJS. Eu encorajo você a verificar outras bibliotecas de asserção e deixar um bilhete se você tem um favorito.

Nota: Esta secção será um pequeno exemplo pesado à medida que mudamos rapidamente de modificar o nosso código e executamos o runner spec frequentemente.

primeiro cenário

ao rever a etapa do primeiro cenário:

Then The grocery list contains a single item

…teremos de provar que a instância da lista de compras cresce por um fator de 1 para cada novo item adicionado.

actualize o passo para definir como esperamos que essa especificação seja validada:

/característica/definições de etapas / add-item.passo.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(); });...};...

nós puxamos o módulo assert e tentamos validar que o comprimento da lista de compras cresceu em um valor de 1 depois de ter executado o passo anterior – quando – na adição do item.Corre isso e teremos uma excepção.:

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

vamos adicionar esse método ao nosso modelo de Lista de compras:

/script / model / Mercearia-list.js

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

e voltar a executar as nossas especificações:

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

visto que o código não está a devolver nada de getAll(), não podemos aceder a uma propriedade length para o nosso teste de asserção.

se modificarmos o código para devolver um Array:

/característica/definições de etapas/add-item.passo.js_

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

e executar as especificações novamente, vamos obter a mensagem de erro de asserção que fornecemos:

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

agora, temos uma falha adequada que nos é relatada a partir de uma afirmação que faz com que o passo não passe. Viva!Vamos parar aqui por um segundo antes de adicionar mais código para fazer este passo passar. O problema em mãos não é realmente adicionar um item ao array que está sendo devolvido, é mais sobre garantir que um item é adicionado através do método add e o resultado de getAll ser uma lista estendida com esse item.

os detalhes de implementação que estão envolvidos em fazer este Passe de teste são onde sua equipe usa sua experiência de arquitetura, mas cuidado é necessário que apenas o código mais essencial é adicionado. Você deve evitar ir borda fora ao pensar sobre os internos do modelo de coleção da lista de compras. É uma corda apertada escorregadia que pode facilmente cair em uma toca de coelho-assim como aquela metáfora mal formulada 🙂

volte ao trabalho!

para os fins deste exemplo, vamos usar o argumento propertiesObject de Object.create para definir um getter list que servirá como uma matriz mutável para os nossos itens da lista de compras:

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

se executarmos isso, descobriremos que o primeiro cenário está agora a passar!Na revisão da etapa final do nosso segundo cenário, a implementação pendente está acessando o item adicionado:

Then I can access that item from the grocery list

para fazer este passo de passo precisamos verificar que podemos acessar o item adicionado à lista de compras invocando add() com um item.

como acontece com a implementação de acessar o comprimento da lista de compras, existem várias maneiras pelas quais podemos fazer este teste passar no código. Novamente, eu sinto que este é o lugar onde o desenvolvimento de software experiência e bom gosto, entra em jogo com relação à arquitetura, mas eu também prefiro a tentar produzir o mínimo de código possível; e eu serei o primeiro a admitir que às vezes me sinto um pouco distraído e criar mais código do que é necessário… por isso, tentar.

dito isto, também temos de ter em conta as especificações de linguagem e ambiente na forma como lidamos com a aprovação da afirmação – e o navegador, com o seu histórico, tem muitos a considerar. Isso não é um pouco, é apenas uma previsão para estabelecer expectativas para os requisitos.

especificamente: suponha que digamos que o passo pode ser verificado usando o método Array.indexOf() na coleção devolvida de’ getAll () ‘ no objeto da lista de compras? Sem um polifill, então estamos nos limitando a passar afirmações no IE 9 e mais velho. Tais considerações são apenas a ponta do iceberg ao decidir sobre o que introduzir em sua base de código, a fim de ter seus testes passar, e realmente deve ser deixado para uma discussão em equipe sobre o que é considerado necessário para obter o produto para a produção.

eu poderia Continuar e continuar, mas vamos apenas assumir que queremos cobrir todas as bases quando se trata de navegadores (ou seja, 6 e acima, arrepios). Na minha opinião, para tornar este segundo cenário verde, vamos adicionar um método getItemIndex() com a seguinte assinatura:

+ getItemIndex(itemValue):int

primeiro modificaremos o passo para falhar.:

/definições das características/etapas/add-item.passo.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();});

a aceitação para que este teste seja aprovado é que o índice em que o item adicionado reside na colecção não é negativo. Para este cenário, não estamos tentando validar uma especificação de onde um novo item é adicionado em uma lista (eg, pré-adicionado ou adicionado), mas simplesmente que ele é acessível.

correr o que irá produzir uma excepção:

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

vamos modificar o nosso objecto da lista de compras para suportar o método getItemIndex:

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

na nossa implementação de getItemIndex, a lista é atravessada e, se o item for encontrado, o índice é devolvido. Caso contrário, um valor de -1 é devolvido. Essencialmente, como o método Array.indexOf funciona do ECMAScript 5.

nota: eu sei que pode parecer tolo usar Objeto.criar a partir do ECMAScript 5, mas não Array.indexOf. A razão – principalmente-sendo que eu normalmente sempre incluir um polifill para objeto.criar e não para Array.indexOf. Suponho que seja hábito.

agora se nós executarmos as especificações novamente sob pepinos:

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

os nossos cukes estão verdes! (Este é o ponto que você limpa sua sobrancelha e palmas lentas).

Conclusion

In this post, I introduced how I use the BDD tool CucumberJS in order to file to Test Driven Development in JavaScript. Eu passei por usando um exemplo de uma única característica com dois cenários e girando passos falhando a cukes verde. Se você não está familiarizado com o processo de fazer testes falham primeiro só para produzir código para fazer o teste passar, eu espero que você seguiu adiante; Eu posso ser wordy e o processo pode parecer levar muito tempo, mas o desenvolvimento sob tais práticas começa a se mover suavemente uma vez que você começa no ritmo. Além disso, eu acho que há uma enorme recompensa em ter seu código sob um arnês de teste quando se trata de refactoração e correção de bugs – tanto na saúde do desenvolvedor e Negócios.

Este artigo foi originalmente publicado em http://custardbelly.com/blog/blog-posts/2014/01/08/bdd-in-js-cucumberjs/index.html

Imagem cortesia de http://commons.wikimedia.org/wiki/File:Og%C3%B3rki…jpg

e6b2cff6a55928c8f62b9d602add3fed?s = 100d=mmr=g

Todd Anderson é um desenvolvedor de aplicações com uma paixão por arquitetura e fluxos de trabalho de desenvolvimento.Como um forte defensor de práticas ágeis e desenvolvimento impulsionado por testes com mais de uma década de experiência, ele ajudou a entregar soluções web, móveis e desktop com inúmeras empresas nas indústrias de entretenimento e empresas, incluindo Adobe, THQ, Condé Nast Publications e Motorola.

ele escreve frequentemente sobre software e Tecnologia em seu blog, suporta software de código aberto e devido ao meu melhor para dar de volta para a comunidade de desenvolvimento através do GitHub e teve o estimado prazer de ser um co-autor em vários títulos de O’Reilly e Wiley Wrox: Amazon profile.Siga Todd no Twitter, visite o site de Todd.

Deixe uma resposta

O seu endereço de email não será publicado.