How to write ATDD tests with cucumber-js, protractor and typescript

Published 10/14/2016 7:01:00 AM
Filed under Typescript

ATDD (Acceptance Test Driven Development) has been around for a while now.
I use it quite a lot on projects that I work on.
It helps me and others translate requirements into automated tests with the minimum amount
of ceremony. We can talk to users about what they want and write that down in a format that
they understand and we can automate.

One of the ways in which I use ATDD is with AngularJS. There is an end-to-end testing
tool for AngularJS called Protractor that supports writing ATDD tests using a testframework
called cucumber-js. It works pretty well with just javascript, but since we use Typescript
a lot more now I figured, why not use typescript for cucumber tests as well?

What is cucumber and why should I care?

For those that don't know cucumber or ATDD for that matter. Cucumber is framework that allows
you to write feature files. A feature file looks like this:

Feature: Basic authentication
Scenario: Users can login using a username and password
  Given I have a user account willem with password somethingElse
  When I login with my username 'willem' and password 'somethingElse'
  Then I am redirected to my personal dashboard

The idea behind ATDD is that you define acceptance tests in a readable format and
automate them using code so that you don't have to execute them manually everytime you
want to validate the functionality of your application.

Using a readable format for your users helps you create a common understanding of what the
application does for the user. Automating these specs into runnable tests closes the gap
between the requirements and the tests. Often times people write specs and create a separate
set of tests. The problem with this is that the tests tend to drift away from the original spec.
This causes bugs that could easily be avoided if you make the specs the tests.

Setting up cucumber with protractor

In order to write acceptance tests for Angular 2 or AngularJS you need to use protractor.
Protractor is a tool that links your tests to a webdriver which ultimately links your tests
to a browser. This is useful since you can now navigate to a page in the application and
communicate with the DOM. That way you can validate your app by querying if the right HTML
elements were shown and much more.

To set protractor up you need to install the following NPM packages on your machine:

  • webdriver-manager
  • protractor

You can install these with the following command:

npm install -g protractor webdriver-manager

Make sure that you update the webdriver-manager after you installed it by running

webdriver-manager update

This will download the proper webdriver files for you so you can start running tests
against the various supported browsers such as IE, Chrome and FireFox.

Next create a new protractor configuration file in your angular project.

exports.config = {
    baseUrl: 'http://localhost:9000',

    // Specify the patterns for test files
    // to include in the test run
    specs: [
        'features/**/*.feature'
    ],

    // Use this to exclude files from the test run.
    // In this case it's empty since we want all files
    // that are mentioned in the specs.
    exclude: [],

    // Use cucumber for the tests
    framework: 'custom',
    frameworkPath: require.resolve('protractor-cucumber-framework'),

    // Contains additional settings for cucumber-js
    cucumberOpts: {

    },

    // These capabilities tell protractor about the browser
    // it should use for the tests. In this case chrome.
    capabilities: {
        'browserName': 'chrome',
        'chromeOptions': {

        }
    },

    // This setting tells protractor to wait for all apps
    // to load on the page instead of just the first.
    useAllAngular2AppRoots: true
}

The file should point to the feature files in your project. I put those in the features
folder, but you can place them somewhere else if you like. Next you need to set the framework
to custom and point protractor to the cucumber-js framework using the frameworkPath setting.

Make sure that you install the protractor-cucumber-framework as part of your project
using the following command:

npm install protractor-cucumber-framework --save-dev

Check your configuration by executing protractor protractor.conf.js. If you have
a feature file in your project you should see protractor spin up a browser and spit out
a bunch of text on the console about your feature file. This usually should involve
a couple of green and yellow lines of text. The green lines show steps in the feature
files that were executed. The yellow lines show steps that aren't implemented yet.

Automating feature files with typescript

The cucumber-js framework is meant to be used with Javascript. In order to do this you
need to write step definitions in javascript files that look like this:

module.exports = function() {
  this.Given(/^I have a user account (.*) with password (.*)$/, function(username, password) {
    //TODO: Do something useful
  });
};

You include them in the protractor config file by specifying a setting called cucumberOpts
that tells the cucumber framework to require your step definition javascript files.

cucumberOpts: {
  require: 'features/step_definitions/**/*.js'
}

I found that javascript is good, but quite annoying to work with since there's so much crap
in the syntax to deal with. People do the weirdest things to their javascript code. Cucumber-js sadly
isn't an exception to that rule.

Typescript offers a much better way of defining step definitions for cucumber-js. But you have to
do a little work for it to function properly.

First you need to install the ts-node and cucumber-tsflow packages in your project
using the following command:

npm install --save-dev cucumber-tsflow ts-node

Next you need to modify the protractor configuration so that it includes the following
cucumberOpts settings:

cucumberOpts: {
    require: ['features/step_definitions/**/*.ts'],
    compiler: 'ts:ts-node/register'
}

This configures cucumber so that it looks for step definitions in the features/step_definitions
folder and tries to load the typescript files directly from there. The problem is that it
can't load typescript files under normal circumstances. However, you can specify a custom compiler
for the cucumber framework. This is where ts-node comes in. When you configure cucumber-js to use
the typescript interface from ts-node it is capable of loading the step definitions.

Time to write some step definitions in typescript:

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

@binding()
class MyStepDefinitions {
  @given(/^I have a user account (.*) with password (.*)$/)
  givenIHaveAUserAccount(username:string, password:string): void {
      //TODO: Do something useful
  }
}

export = MyStepDefinitions;

This looks way easier to read and I can assure you it's much more ergonomic to write.
And luckely it is not much work to set up in the end.