TypeScript & Cucumber – Getting started

cucumber

In february, I did a presentation with my friend Julien Pavon at Paris TypeScript meetup on Behavior Driven Development. It was really great to talk about this methodology and feedbacks from attendees were really constructive. We explained what is BDD and how to use it with TypeScript. To prepare this speech, I used the new version of Cucumber.js (one of the most popular BDD framework). To be honest I found that previous Cucumber versions have some issues but the version 4 fixes a lot of them… It’s now really better and I have to admit that I love this version! Let’s take a look on that 😉

Behavior Driven Development

First, this post isn’t a complete description of how to use Behavior Driven Development in a efficient way, but I have to describe a little bit this methodology for developers who don’t know it.

It was designed by Dan North in the early 2000s. As he said, BDD is “TDD done well”. The methodology brings a really important concept inside the development lifecycle: Communication between people who work on the project.

Basically it’s not just a technical topic. For developers it’s the same as Test Driven Development, for a product owner it’s all about the domain and how it works in a software. To easily describe that you can use Gherkin. This language defines three main keywords:

  • Given: Describe the context of a scenario (“Arrange” in TDD)
  • When: Do an action in the system (“Act” in TDD)
  • Then: Check that expected results have occurred (“Assert” in TDD)

By using those keywords, you can write a scenario that describes a behavior in the system.

Scenario example

Here is a basic example of a calculator divide operator. This kind of examples are really common to start with BDD:

Feature: As a user, i want to divide two numbers

Scenario: Divide two numbers
    Given a calculator
    When I divide 6 by 2
    Then the result is 3

This feature is really basic but it only describes a behavior. Sometime you can find samples with this kind of steps:

When I press the total button
Then the result should be "120" on the screen

If you do that, just keep in mind that those steps describe a user interface, not a behavior.

Setup

Before creating the project structure, make sure that Node.js is available on your environment and TypeScript is globally installed with npm. Then, create the project folder:

$ mkdir typescript-cucumber && cd typescript-cucumber

Initialize both npm and TypeScript configuration:

$ npm init -y
$ tsc --init

Cucumber must be installed as a development dependencies. In my sample I also use chai for my assertions. Both packages don’t embedded TypeScript definitions, so you need to install them.

$ npm i -D cucumber chai @types/cucumber @types/chai

Now, create the project structure:

$ mkdir src && mkdir features && mkdir features/steps

Inside the “features” folder, create a “divide.feature” file and copy/paste the feature  from the previous part of this post. Finally, you need to add a “test” script inside the package.json to run all scenarios:

"test": "tsc && cucumber-js features/"

Show me the code!

The divide feature is really simple to implement,  we just have three different steps. Inside steps folder, create a divideSteps.ts file. To handle every steps of the scenario, add the following code inside the TypeScript file:

import { expect } from "chai";
import { Given, When, Then } from "cucumber";

Given("a calculator", function() {
    throw new Error("Not implemented");
});

When("I divide {int} by {int}", function(n1: number, n2: number) {
    throw new Error("Not implemented");
});

Then("the result is {int}", function(expected: number) {
    throw new Error("Not implemented");
});

As you can see, I used patterns to match typed parameters of the scenario. This kind of syntax is available in Cucumber since the version 3 and it increases readability. That’s really a great way to implement your steps.

Cucumber describes current scenario context as “World”. It can be used to store the state of the scenario context (you can also define helper methods in it). World can be access by using the this keyword inside step functions (that’s why it’s not recommended to use arrow functions). Calculator isn’t implemented yet (don’t forget that BDD is TDD, we must write tests first) and World doesn’t have any typed members. So you are going to design the calculator directly inside the When step. You probably should ask: How? To be honest it’s really simple, just design the method as you want to use it:

When("I divide {int} by {int}", function(n1: number, n2: number) {
    this.actual = this.calculator.divide(n1, n2);
});

No complexity here, you just need a method that accepts two numbers and returns the division result between them. You also need to store this result inside the current scenario context (I used “actual” as property name to respect the actual-expected naming convention). Of course there is no calculator instance inside the context, let’s define it:

Given("a calculator", function() {
    this.calculator = {
        divide(n1: number, n2: number): number {
            throw new Error("Not implemented");
        }
    };
});

Finally, add an assertion inside the Then step to ensure that division result corresponds to the expected value:

Then("the result is {int}", function(expected: number) {
    expect(this.actual).to.be.equal(expected);
});

Calculator is now designed. Of course if you run the scenario, it fails because divide method doesn’t have any implementation. It’s time to switch from fail to success by implementing the divide method. Just add this line of code inside the method:

return n1 / n2;

Now, let’s refactor the code. Inside the Given step, you need to manipulate the calculator property with a type. Create a calculator.ts file inside “src” folder and add the Calculator class that corresponds to the literal object you design inside the Given step:

export class Calculator {
    divide(n1: number, n2: number): number {
        return n1 / n2;
    }
}

With TypeScript you can redefine a type inside a module. This is a good option to ensure you can manipulate a typed member to the World interface. Inside the “features” folder, create a “world” folder and add a calculatorWorld.ts file that contains the following code:

import { Calculator } from "../../src/calculator";
import { World } from "cucumber";

declare module "cucumber" {
    interface World {
        calculator: Calculator;
        actual: number;
    }
}

Inside divideStep.ts, import the Calculator class:

import { Calculator } from "../../src/calculator";

Then, change the Given step by creating a new instance of the Calculator class and assign it to the dedicated property:

Given("a calculator", function() {
    this.calculator = new Calculator();
});

As you can notice, VS Code now retrieve World members with types. That’s why redefine the World instance is really useful 🙂

ts-cucumber-vscode-1

Alright, execute the npm “test” script to ensure every steps succeed.

Debug with VS Code

To be honest with you, I saw a lot of people that have issues with tests execution and debugging with TypeScript. In reality it’s very simple, just add a “build” script in your package.json.

"build": "tsc"

Install the npm extension from the marketplace (eg2.vscode-npm-script):

ts-cucumber-vscode-2

Open the debug panel and add a new debug configuration:

ts-cucumber-vscode-3

Select Node.js as the debug environment.

ts-cucumber-vscode-4

Open launch.json, and update the debug configuration to use cucumber-js. A pre-launch task can be define to build the project before tests execution (that’s why we need the npm extension).

{
    "type": "node",
    "request": "launch",
    "name": "Debug",
    "program": "${workspaceFolder}/node_modules/cucumber/bin/cucumber-js",
    "args": [
        "features/"
    ],
    "console": "integratedTerminal",
    "internalConsoleOptions": "neverOpen",
    "preLaunchTask": "npm: build"
}

Inside tsconfig.json set sourcemap to true.

"sourcemap": true

As you can see, it’s really straightforward. Of course you can do the same with your favorite test runners (Jest, Mocha, Jasmine, …)

Conclusion

I hope you enjoyed this post. Now you can create basic features and implement them with Cucumber. Of course the sample is too simple to reflect a complex project organization. I’m planning to publish another post on this topic with an advanced scenario (maybe a Node.js microservice).

You can find the complete sample in this repository.

CYA

 

12 thoughts on “TypeScript & Cucumber – Getting started

  1. Hi! Thank you, but I think this can’t be used in a real app. All .ts files will generate a .js in your source code when you run this project. Any proper solution?
    Cheers

    1. Hi!

      I don’t really understand your point, what’s wrong for you?
      Also keep in mind that it’s just a getting started post and it can’t be considered as a complete solution for production environment.

      1. So it’s fine to have source code in TS alongside with js files generated?

        Like “src/myfile.ts”, “src/myfile.js”. Then when you commit you need delete all these files that are mixed in your source code, or add to gitignore. Both bad solutions

        I understand is a starter but can’t be used for a real project.

      2. Ok I see.

        Yeah it’s bad to keep TypeScript files in your production environment. I personally don’t commit JavaScript files in my git repository, because these are generated files and not real source files.

        To deal with production environment, you can:
        – Define an output directory in tsconfig.json, but you have to switch the sourceMap property to disable it every time you change the environment (debug/production)… I’m not a huge fan of that, too manual.
        – Create a build script with your favorite task runner. You can define specific tsc configuration for production inside the script and keep the tsconfig.json for the debug environment only. You can find an example here: https://github.com/spontoreau/rust-vsts/blob/master/gulpfile.js. This solution is also great for CI process.

        Both are some example, there are a lot of options in reality but this post only cover the debug part of a TypeScript project.

      3. I’m talking about cucumber and typescript: if you build the files in a dist folder cucumber can’t find your step definitions. So you need to manually copy paste the feature files in a temp folder and run cucumber from the temp folder

      4. If you are forced to build your application in a dist folder, even for debug, you’re in the worse case with cucumber. Maybe you have to consider enhance your npm script that compile the project to copy the feature files for you, it seems the easiest way to resolve this kind of problem.

      5. I think is worse to build in your source code, like in your example, than in a dist folder. Seems like you don’t get my point.

        So your cucumber example works like this: you compile to js, these js files are spread in your source code, not in a separate temporary folder, cucumber will read the is files.

        Do you see how bad is it?

      6. It’s not worse or bad, it depends on your project & team organisation. Currently you’re thinking that, but it’s a personal opinion. For me it’s different, I don’t care about that because in all my projects what I’ve got in my source code is not what I run in production. I have a continuous integration process that prepare and clean my dist before deploying it. So the generated files can be in a dist or in a source folder, I don’t care, it will not be in the production version of my app. That’s all 🙂

        I know how my cucumber example work. It’s just a debug one, not a production one and it doesn’t reflect how I will build my application for production.

  2. Hi Sylvain,
    Great post! It helped me.

    To Tt’s point I was able to overcome with below script commands:

    “scripts”: {
    “build”: “tsc”,
    “postbuild”: “cp -r features/ ./dist/features/”,
    “test”: “npm run build && cucumber-js — dist/features –require dist/features/steps”
    }

    Thanks,
    Madhan

  3. Thanks Sylvain. How can I use “background” in my Gherkin feature file. What library should I import.
    It is currently failing when I use “background” keyword.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s