Getting started with Cucumber for JavaScript

Filed under: Cucumber, — Tags: Behaviour-Driven Development, Cucumber-js, Gherkin, JavaScript — Thomas Sundberg — 2018-02-07

I usually describe myself as a Java backend developer. That is actually not completely true. I tend not to shy away from the frontend, but I prefer the backend because it is usually much easier to test.

This backend focus has allowed me to almost entirely miss out on JavaScript development. I can't say that I mind that much. I use JavaScript when a web page needs it, but I use Google a lot to find examples I can modify to fit my needs.

One thing I am curious about though is to be able to use Cucumber-js. I have never done it before so this a blog post that will describe my experience getting started.

I share this experience because one way for me to learn is to teach. I don't have a student this time, I have a blog post. A blog post will, as a group of students, force me to be as clear as I can be. And before I publish it, I will have it reviewed by someone who knows the topic as well as a novice or two.

I want to reduce the number of tools I use. Using too many tools will be confusing and move focus from the actual goal, getting Cucumber-js up and running.

This means that I will do everything from a a command line. You need an editor, any editor will be fine. I use InteliJ IDEA as my general purpose editor. You may have another favorite. Use an editor you are comfortable with.

Installing

Cucumber-js depends on node.js.

My first step is to install node.js. There are different ways to do this. I'm on a Mac so I use Homebrew. All I had to do was brew install node in a terminal. You may have to do something else on your operating system.

Node.js

To verify that I have node.js installed properly, I type node -v in a terminal.

$ node -v
v9.4.0

This tells me that I have node.js installed, The latest version as I write this is 9.4.0. I'm up to date.

I also need a JavaScript package manager called npm. It should be installed when node.js is installed. To verify that npm is installed and check its version I do npm -v

$ npm -v
5.6.0

Looks good, I should be ready to install Cucumber-js now.

Cucumber-js

Before installing Cucumber-js, I need a working directory for the rest of this tutorial. Create a new directory called cucumber-js and change into it. The rest of tutorial assumes that you are in this directory.

Cucumber-js is available as an npm package. This allows me to install it using npm install cucumber.

$ npm install cucumber

> cucumber-expressions@5.0.13 postinstall /Users/tsu/cucumber-js/node_modules/cucumber-expressions
> node scripts/postinstall.js

npm WARN saveError ENOENT: no such file or directory, open '/Users/tsu/cucumber-js/package.json'
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN enoent ENOENT: no such file or directory, open '/Users/tsu/cucumber-js/package.json'
npm WARN example No description
npm WARN example No repository field.
npm WARN example No README data
npm WARN example No license field.

+ cucumber@4.0.0
added 66 packages in 2.483s

A lot of things happened here. I'm not entirely sure about the details. But the Cucumber documentation tells me that I should be able to run Cucumber if I do ./node_modules/.bin/cucumber-js

The result is:

$ ./node_modules/.bin/cucumber-js


0 scenarios
0 steps
0m00.000s

Nothing bad happened and it was quick. I guess that's a good start.

My first scenario

With an environment prepared and verified, it's time to create a first scenario.

Scenarios live in the directory features. My first scenario is a really boring one, adding two numbers. It is boring but it allows me to focus on getting the infrastructure up and running. And that is the main purpose with this exercise.

I create the file features/addition.feature with this content:

Feature: Addition

  Addition is great as a verification exercise to get the Cucumber-js infrastructure up and running

  Scenario: Add two number
    Given the numbers 1 and 3
    When they are added together 
    Then should the result be 4

Next step is to run Cucumber and see what happens. I expect some hints on creating steps.

$ ./node_modules/.bin/cucumber-js
UUU

Warnings:

1) Scenario: Add two number # features/addition.feature:5
   ? Given the numbers 1 and 3
       Undefined. Implement with the following snippet:

         Given('the numbers {int} and {int}', function (int, int2, callback) {
           // Write code here that turns the phrase above into concrete actions
           callback(null, 'pending');
         });
       
   ? When they are added together
       Undefined. Implement with the following snippet:

         When('they are added together', function (callback) {
           // Write code here that turns the phrase above into concrete actions
           callback(null, 'pending');
         });
       
   ? Then should the result be 4
       Undefined. Implement with the following snippet:

         Then('should the result be {int}', function (int, callback) {
           // Write code here that turns the phrase above into concrete actions
           callback(null, 'pending');
         });
       

1 scenario (1 undefined)
3 steps (3 undefined)

As a JavaScript newbie but not a Cucumber newbie, I got pretty much what I expected. I got UUU which I assume means that there are three undefined steps. I also got three snippet suggestions.

My next task is to implement the missing steps. Looking at examples on GitHub, it seems as if the convention is to store the steps in a directory called features/step_definitions/. I create the file features/step_definitions/addition_steps.js with the snippets suggested.

The result looks like this:

Given('the numbers {int} and {int}', function (int, int2, callback) {
    // Write code here that turns the phrase above into concrete actions
    callback(null, 'pending');
});


When('they are added together', function (callback) {
    // Write code here that turns the phrase above into concrete actions
    callback(null, 'pending');
});

Then('should the result be {int}', function (int, callback) {
    // Write code here that turns the phrase above into concrete actions
    callback(null, 'pending');
});

Running Cucumber again gives me this:

$ ./node_modules/.bin/cucumber-js
ReferenceError: Given is not defined
    at Object.<anonymous> (/Users/tsu/cucumber-js/features/step_definitions/addition_steps.js:1:63)
    at Module._compile (module.js:660:30)
    at Object.Module._extensions..js (module.js:671:10)
    at Module.load (module.js:573:32)
    at tryModuleLoad (module.js:513:12)
    at Function.Module._load (module.js:505:3)
    at Module.require (module.js:604:17)
    at require (internal/module.js:11:18)
    at /Users/tsu/cucumber-js/node_modules/cucumber/lib/cli/index.js:163:16
    at Array.forEach (<anonymous>)
    at Cli.getSupportCodeLibrary (/Users/tsu/cucumber-js/node_modules/cucumber/lib/cli/index.js:162:24)
    at Cli.<anonymous> (/Users/tsu/cucumber-js/node_modules/cucumber/lib/cli/index.js:181:39)
    at Generator.next (<anonymous>)
    at Generator.tryCatcher (/Users/tsu/cucumber-js/node_modules/bluebird/js/release/util.js:16:23)
    at PromiseSpawn._promiseFulfilled (/Users/tsu/cucumber-js/node_modules/bluebird/js/release/generators.js:97:49)
    at Promise._settlePromise (/Users/tsu/cucumber-js/node_modules/bluebird/js/release/promise.js:574:26)

Something is clearly missing here. With a Java background, I have the feeling that I need to make some dependencies available. How do you do that?

By adding the line const { Before, Given, When, Then } = require('cucumber') at the top of my JavaScript file I seem to be able to define the Gherkin keywords that were not defined.

features/step_definitions/addition_steps.js now looks like this:

const { Before, Given, When, Then } = require('cucumber')

Given('the numbers {int} and {int}', function (int, int2, callback) {
    // Write code here that turns the phrase above into concrete actions
    callback(null, 'pending');
});


When('they are added together', function (callback) {
    // Write code here that turns the phrase above into concrete actions
    callback(null, 'pending');
});

Then('should the result be {int}', function (int, callback) {
    // Write code here that turns the phrase above into concrete actions
    callback(null, 'pending');
});

Running Cucumber again:

$ ./node_modules/.bin/cucumber-js
P--

Warnings:

1) Scenario: Add two number # features/addition.feature:5
   ? Given the numbers 1 and 3 # features/step_definitions/addition_steps.js:3
       Pending
   - When they are added together # features/step_definitions/addition_steps.js:9
   - Then should the result be 4 # features/step_definitions/addition_steps.js:14

1 scenario (1 pending)
3 steps (1 pending, 2 skipped)
0m00.002s

This is much better. It doesn't work, but now I'm getting a P which I am sure means that there is a pending step.

A pending step means that there is a step found, but that it isn't properly implemented yet. Next task is to implement this pending step.

Implementing the first step - create and setup a calculator

The first step should prepare some kind of calculator and feed it two numbers.

I will create a lib directory at the same level as the feature directory and add a file called calculator.js in it that will contain a calculator.

My calculator.js now looks like this:

class Calculator {

}

module.exports = Calculator;

As you can see, it is currently just an empty skeleton. I want to make the Calculator available to my steps. I do that by require the Calculator in my steps implementations. My steps have now evolved to this:

const { Before, Given, When, Then } = require('cucumber');
const Calculator = require('../../lib/calculator');

let calculator;

Given('the numbers {int} and {int}', function (int, int2, callback) {
    calculator = new Calculator();
    callback();
});

When('they are added together', function (callback) {
    // Write code here that turns the phrase above into concrete actions
    callback(null, 'pending');
});

Then('should the result be {int}', function (int, callback) {
    // Write code here that turns the phrase above into concrete actions
    callback(null, 'pending');
});

I require calculator.js and define a constant Calculator. This the equivalent of an import in Java.

I also declare a variable called calculator to keep an instance of a calculator between the steps.

Finally, in the Given step, I create a new Calculator and assign it to the field

Running Cucumber again gives me this result:

$ ./node_modules/.bin/cucumber-js
.P-

Warnings:

1) Scenario: Add two number # features/addition.feature:5
   ✔ Given the numbers 1 and 3 # features/step_definitions/addition_steps.js:6
   ? When they are added together # features/step_definitions/addition_steps.js:11
       Pending
   - Then should the result be 4 # features/step_definitions/addition_steps.js:16

1 scenario (1 pending)
3 steps (1 pending, 1 skipped, 1 passed)
0m00.003s

The first step passes. The second step is now pending. I'm not done with the first step, but I seem to be able to run Cucumber without errors. That's a good start!

My next step is to declare two variables in the calculator and assign them in the constructor.

I'll call the variables x and y

The calculator now looks like this:

class Calculator {

    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

}

module.exports = Calculator;

I have a constructor that accepts two values. These two values are used in the Given step. The steps now looks like this:

const { Before, Given, When, Then } = require('cucumber');
const Calculator = require('../../lib/calculator');

let calculator;

Given('the numbers {int} and {int}', function (x, y, callback) {
    calculator = new Calculator(x, y);
    callback();
});

When('they are added together', function (callback) {
    // Write code here that turns the phrase above into concrete actions
    callback(null, 'pending');
});

Then('should the result be {int}', function (int, callback) {
    // Write code here that turns the phrase above into concrete actions
    callback(null, 'pending');
});

I used the new constructor and I renamed the parameters x and y.

This should be enough for my first step. Next up is to clean the pending steps and implement the second step, using the calculator.

Implementing the second step - use the calculator

My first action is to clean up a bit. My tests are passing so I can do changes without the risk of introducing problems. I would like to get rid of the pending steps. I also don't understand what the callback parameter is used for. What happens if I just remove it? Some experimenting ended up with me removing callback parameter. This resulted in two things, a bit cleaner code and the pending steps removed.

My steps now look like this:

const { Before, Given, When, Then } = require('cucumber');
const Calculator = require('../../lib/calculator');

let calculator;

Given('the numbers {int} and {int}', function (x, y) {
    calculator = new Calculator(x, y);
});

When('they are added together', function () {
});

Then('should the result be {int}', function (int) {
});

This looks much better to me. I didn't really understand what the parameter callback was used for. My current understanding is that it was used for reporting back to Cucumber that this step is pending.

After feedback I have learned that the callback is the original way of dealing with non-blocking asynchronous operations in Node.js and was the original interface to step definitions. The different interfaces supported nowadays are documented here: https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/step_definitions.md

It can be argued that the cleaning I did at this stage is premature. It may be better to keep the pending steps until the they are properly implemented. Cleaning this up early can trick a beginner to believe we're done. I will mitigate this risk by deliberately fail the execution before I'm done and make sure that the failure is what I expected. I will do this in my last step, the Then step that is empty above.

Running Cucumber now gives me this:

$ ./node_modules/.bin/cucumber-js
...

1 scenario (1 passed)
3 steps (3 passed)
0m00.000s

This is great, I have a passing build! It is just a bit unfortunate that I know that it's just an empty execution. We don't do anything useful yet.

Let's make one useful thing, lets use the calculator so we can assert that a calculation actually was done in the last step.

I call calculator.add() in the When step

const { Before, Given, When, Then } = require('cucumber');
const Calculator = require('../../lib/calculator');

let calculator;

Given('the numbers {int} and {int}', function (x, y) {
    calculator = new Calculator(x, y);
});

When('they are added together', function () {
    calculator.add();
});

Then('should the result be {int}', function (int) {
});

I also implement the corresponding add() in the calculator. My new Calculator looks like this:

class Calculator {

    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    add() {
        this.result = this.x + this.y;
    }

}

module.exports = Calculator;

Running cucumber is unchanged, everything passes. But I know that there is no assert yet. Lets add a verification of the result in the last step.

Implementing the third step - verify the calculation

The first thing I do is to add getting the result in the Then step. I also need to make assert available to my steps. The last version of the steps look like this:

const assert = require('assert')
const {Before, Given, When, Then} = require('cucumber');
const Calculator = require('../../lib/calculator');

let calculator;

Given('the numbers {int} and {int}', function (x, y) {
    calculator = new Calculator(x, y);
});

When('they are added together', function () {
    calculator.add();
});

Then('should the result be {int}', function (expected) {
    assert.equal(calculator.getResult(), expected)
});

This forces me to implement the last part in the calculator, retrieving the result. The calculator now becomes:

class Calculator {

    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    add() {
        this.result = this.x + this.y;
    }

    getResult() {
        return this.result;
    }
}

module.exports = Calculator;

Running Cucumber still looks good:

$ ./node_modules/.bin/cucumber-js
...

1 scenario (1 passed)
3 steps (3 passed)
0m00.001s

There is no difference compared to the last execution. How can we be sure that it works? I never trust a test I haven't seen fail. Let's fail the execution verify that it is actually comparing the calculated value with the expected value.

The simplest way I can think of is to change the feature. Let's expect 5 instead of 4. The feature will look like this now:

Feature: Addition

  Addition is great as a verification exercise to get the Cucumber-js infrastructure up and running

  Scenario: Add two number
    Given the numbers 1 and 3
    When they are added together
    Then should the result be 5

Running cucumber now looks like this:

$ ./node_modules/.bin/cucumber-js
..F

Failures:

1) Scenario: Add two number # features/addition.feature:5
   ✔ Given the numbers 1 and 3 # features/step_definitions/addition_steps.js:7
   ✔ When they are added together # features/step_definitions/addition_steps.js:11
   ✖ Then should the result be 5 # features/step_definitions/addition_steps.js:15
       AssertionError [ERR_ASSERTION]: 4 == 5
           + expected - actual

           -4
           +5
       
           at World.<anonymous> (/Users/tsu/cucumber-js/features/step_definitions/addition_steps.js:16:12)

1 scenario (1 failed)
3 steps (1 failed, 2 passed)
0m00.001s

We have a failure. This failure is a success as it show that the value in the Then step actually is used.

Change it back and run Cucumber again. You should now have a passing scenario.

Wrap up

If you have followed along this far, you have seen how a seasoned Java developer was able to get Cucumber up and running. I must admit that I have searched the Internet for examples before I came up with this solution. I have also looked at some material supplied by Cucumber Ltd.

Acknowledgements

I would like to thank

for feedback and finding typos I missed.

Thank you very much!

Resources

Bonus material

A reviewer wondered, how do you put this under version control? All serious development is done using version control. This is very a valid question.

The consultant answer is it depends. I use git for version control. With git you do:

More details are out of the scope in this tutorial. Better tutorials can be found using Google.



(less...)

Pages

About
Events
Why

Categories

Agile
Automation
BDD
Clean code
Continuous delivery
Continuous deployment
Continuous integration
Cucumber
Culture
Design
DevOps
Executable specification
Gradle
Guice
J2EE
JUnit
Java
Javascript
Kubernetes
Linux
Load testing
Maven
Mockito
New developers
Pair programming
PicoContainer
Presentation
Programming
Public speaking
Quality
React
Recruiting
Requirements
Scala
Selenium
Software craftsmanship
Software development
Spring
TDD
Teaching
Technical debt
Test automation
Tools
Web
Windows
eXtreme Programming

Authors

Thomas Sundberg
Adrian Bolboaca

Archives

Meta

rss RSS