loading...

Cucumber.js with TypeScript

denolfe profile image Elliot DeNolf Originally published at elliotdenolf.com on ・4 min read

Originally posted on my blog at https://elliotdenolf.com/posts/cucumberjs-with-typescript

Cucumber.js is the JavaScript implementation of Cucumber. The main benefit of writing automated tests for Cucumber is that they are written in plain English, so any non-technical person can read the scenarios and know what is being tested. This is extremely powerful in larger organizations because it allows developers, testers, and business stakeholders to better communicate and collaborate.

This post will go through setting up a basic Cucumber.js suite using TypeScript and cucumber-tsflow. Cucumber-tsflow is a package that will allow us to take advantage of TypeScript’s decorators, which make for clearer step definition code.

The first step will be installing our dependencies:

npm i -D cucumber cucumber-tsflow cucumber-pretty ts-node typescript chai
npm i -D @types/cucumber @types/chai

experimentalDecorators must also be set to true in your tsconfig.json in order for the decorators to compile properly.

The two main components for cucumber tests are feature files and step definitions. Let’s start out by creating a features directory then creating a file named bank-account.feature inside it. Our example will be testing the basic functionality of a bank account.

# features/bank-account.feature
Feature: Bank Account

  Scenario: Stores money
    Given A bank account with starting balance of $100
    When $100 is deposited
    Then The bank account balance should be $200

This defines a single scenario for depositing money into a bank account. Next, we will create a directory named step-definitions and create a file named bank-account.steps.ts within it.

import { binding, given, then, when} from 'cucumber-tsflow';
import { assert } from 'chai';

@binding()
export class BankAccountSteps {
  private accountBalance: number = 0;

  @given(/A bank account with starting balance of \$(\d*)/)
  public givenAnAccountWithStartingBalance(amount: number) {
    this.accountBalance = amount;
  }

  @when(/\$(\d*) is deposited/)
  public deposit(amount: number) {
    this.accountBalance = Number(this.accountBalance) + Number(amount);
  }

  @then(/The bank account balance should be \$(\d*)/)
  public accountBalanceShouldEqual(expectedAmount: number) {
    assert.equal(this.accountBalance, expectedAmount);
  }
}

We are utilizing the cucumber-tsflow package which exposes some very useful decorators for our Given, When, and Then steps. The code within each step is fairly simple. The Given step initializes the accountBalance, the When step adds to the balance, and the Then step asserts its value.

Some specific things to note: this file exports a single class which has the @binding() decorator on it which is required for cucumber-tsflow to pick up the steps. Each step definition must also have a @given, @when or @then decorator on it. These decorators take a regular expression as a parameter which is how the lines in the feature file map to the code. Also, make note that there are capture groups in the expressions to capture values from the text and are subsequently passed as parameters to the function.

Cucumber is run using the cucumber-js command with a series of command-line switches. However, this can optionally be put into a cucumber.js file at the root of the project. Create a cucumber.js file at the root of the project with the following contents:

// cucumber.js
let common = [
  'features/**/*.feature', // Specify our feature files
  '--require-module ts-node/register', // Load TypeScript module
  '--require step-definitions/**/*.ts', // Load step definitions
  '--format progress-bar', // Load custom formatter
  '--format node_modules/cucumber-pretty' // Load custom formatter
].join(' ');

module.exports = {
  default: common
};

Putting the configuration in this file allows us to simply pass the profile name to cucumber-js (default in our case) instead of a long list of arguments. This file is building out all of the command line arguments, joining them, then exporting them under a named property. Let’s add an npm script to our package.json, so we can easily run it.

// package.json
{
  // ...
  "scripts": {
    "test": "./node_modules/.bin/cucumber-js -p default"
  },
  // ...
}

The structure of your project should now look like this:

.
|-- cucumber.js
|-- features
| `-- bank-account.feature
|-- package.json
|-- step-definitions
| `-- bank-account.steps.ts
`-- tsconfig.json

Now when we run npm test, cucumber-js inside of our node_modules will be executed with the -p default switch denoting the default profile exported from our cucumber.js file we created earlier.

The output should be something similar to this:

Feature: Bank Account

  Scenario: Stores money
    Given A bank account with starting balance of $100
    When $100 is deposited
    Then The bank account balance should be $200

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

That’s it! You’re up and going with Cucumber and TypeScript!

Links

Discussion

pic
Editor guide
Collapse
domorodec profile image
Martin

Hello, could you pls tell me what is advantage instead of using cucumber scenario? I don´t understand what is plus to use cucumber-tsflow.

We write scenarion in cucumber like -
Then Change user to MO
And I accept the request from private inbox with state xxx

And code look the same as in your example of course with different logic. I can also even create class and make it export to use it on more places so .... I don´t know why to use cucumber-tsflow.

Collapse
denolfe profile image
Elliot DeNolf Author

The differences will be how step definitions are written, not the feature files.

The main benefit I see is that it provides type annotations, which allow the code to be extremely clean and understandable. Without these annotations, the code must have a lot of wrapping functions, which make the code harder to re-use. The documentation shows some examples worth looking at.

Collapse
domorodec profile image
Martin

Why this shows me error telling that declaration expected after @waitForSpinnerToEnd()

@then(/I wait for spinner to end/)
public static waitForSpinner() {
@waitForSpinnerToEnd()
}

export const waitForSpinnerToEnd = async () => {
await World.page.waitFor(200);
await World.page.waitFor(() => !document.querySelector('.spinner-three-bounce'), {visible: true});
};

Collapse
ohadr profile image
OhadR

question regarding Timeouts. In cucumber's steps it is possible to change the timeout per step (read here: github.com/cucumber/cucumber-js/bl...). I cannot figure how it is possible to do so using the cucumber-tsflow :-(

any idea?

Collapse
bmoescoderoom profile image
Bmoe

Hi. How would I mock objects with this cucumber setup? Jasmine provides spies out of the box but cucumber does not.

Collapse
ohadr profile image
OhadR

great article.

worth to mention hooks... i.e. Before ==>@before in the Step file

Collapse
pnakhat profile image
pnakhat

Hi can you put JSON and HTML reporter example in this project (Using typescript ?)

Collapse
denolfe profile image
Elliot DeNolf Author

The link to the full source is at the bottom of the article. Take a look at the cucumber.js file.