~/blog|
Published on

Cucumber.js with TypeScript

Authors

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!