erik lieben

Erik Lieben software developer @Effectory

Protractor e2e testing with Gherkin, CucumberJS, Typescript.

Published , 9 min read (1863 words)

In the following blog post I will show you how to setup Protractor e2e testing using TypeScript and Gherkin syntax.

Create a new folder and add the following folders:

Create a default package file by performing:

shell
npm init -y
shell
npm init -y

Install all the packages that are required:

shell
npm i chai cucumber cucumber-tsflow gulp gulp-clean gulp-protractor gulp-protractor-cucumber-html-report gulp-typescript protractor protractor-cucumber-framework require-dir typings --save-dev
shell
npm i chai cucumber cucumber-tsflow gulp gulp-clean gulp-protractor gulp-protractor-cucumber-html-report gulp-typescript protractor protractor-cucumber-framework require-dir typings --save-dev

This will install the following npm packages:

Setup gulp #

In the root folder create a file named gulpfile.ts and add the following content:

typescript
// all gulp tasks are located in the ./build/tasks directory
// gulp configuration is in files in ./build directory
require('require-dir')('build/tasks');
typescript
// all gulp tasks are located in the ./build/tasks directory
// gulp configuration is in files in ./build directory
require('require-dir')('build/tasks');

Setup typescript transpile #

Create a build.js file in the directory build\tasks and add the following content:

typescript
var gulp = require("gulp");
var ts = require("gulp-typescript");
var tsProject = ts.createProject('tsconfig.json');
var clean = require("gulp-clean");
var paths = require('../paths');
gulp.task("clean", function () {
return gulp.src(paths.dist, { read: false }).pipe(clean());
});
gulp.task("build", ["clean"], function () {
var tsResult = tsProject.src().pipe(ts(tsProject));
return tsResult.js.pipe(gulp.dest(paths.dist));
});
typescript
var gulp = require("gulp");
var ts = require("gulp-typescript");
var tsProject = ts.createProject('tsconfig.json');
var clean = require("gulp-clean");
var paths = require('../paths');
gulp.task("clean", function () {
return gulp.src(paths.dist, { read: false }).pipe(clean());
});
gulp.task("build", ["clean"], function () {
var tsResult = tsProject.src().pipe(ts(tsProject));
return tsResult.js.pipe(gulp.dest(paths.dist));
});

In the build folder add a file named paths.js and add the following content:

typescript
module.exports = { dist: "dist/" };
typescript
module.exports = { dist: "dist/" };

This will be the location where you store all the paths to the resources used in your build scripts. Next add the configuration settings for Typescript by adding a file named tsconfig.json in the root folder of your project with the following content:

json
{
"compilerOptions": {
"moduleResolution": "node",
"module": "umd",
"target": "es5",
"declaration": true,
"sourceMap": true,
"removeComments": false,
"experimentalDecorators": true
},
"exclude": [ "node_modules" ],
"filesGlob": [ "steps/**/*.ts" ]
}
json
{
"compilerOptions": {
"moduleResolution": "node",
"module": "umd",
"target": "es5",
"declaration": true,
"sourceMap": true,
"removeComments": false,
"experimentalDecorators": true
},
"exclude": [ "node_modules" ],
"filesGlob": [ "steps/**/*.ts" ]
}

Add typings definitions for the javascript libs we are using:

shell
typings i dt~chai dt~angular-protractor dt~selenium-webdriver --save --global
shell
typings i dt~chai dt~angular-protractor dt~selenium-webdriver --save --global

Setup Protractor #

In the build folder add a file named test.js and add the following content:

typescript
var gulp = require("gulp");
var webdriver_update = require("gulp-protractor").webdriver_update;
var protractor = require("gulp-protractor").protractor;
var reporter = require("gulp-protractor-cucumber-html-report");
var paths = require('../paths');
gulp.task("webdriver_update", webdriver_update);
gulp.task("e2e", ["build"], function () {
return gulp
.src(paths.features)
.pipe(protractor({ configFile: "protractor.conf.js" }))
.on("error", function (e) { throw e; });
});
gulp.task("e2e-report", function () {
gulp.src(paths.testResultJson)
.pipe(reporter({ dest: paths.e2eReports }));
});
typescript
var gulp = require("gulp");
var webdriver_update = require("gulp-protractor").webdriver_update;
var protractor = require("gulp-protractor").protractor;
var reporter = require("gulp-protractor-cucumber-html-report");
var paths = require('../paths');
gulp.task("webdriver_update", webdriver_update);
gulp.task("e2e", ["build"], function () {
return gulp
.src(paths.features)
.pipe(protractor({ configFile: "protractor.conf.js" }))
.on("error", function (e) { throw e; });
});
gulp.task("e2e-report", function () {
gulp.src(paths.testResultJson)
.pipe(reporter({ dest: paths.e2eReports }));
});

This will allow you to use gulp to start the running the protractor test. Extend your paths.js file to include the new paths used above:

typescript
module.exports = {
dist: "dist/",
features: "features/**/*.feature",
testResultJson: "./reports/cucumber-test-results.json",
e2eReports: "reports/e2e"
};
typescript
module.exports = {
dist: "dist/",
features: "features/**/*.feature",
testResultJson: "./reports/cucumber-test-results.json",
e2eReports: "reports/e2e"
};

In the root folder of your project add the file protractor.conf.js and add the following content:

typescript
var paths = require('build/paths');
exports.config = {
directConnect: true,
multiCapabilities: [ {'browserName': 'firefox'}, {'browserName': 'chrome'}, {'browserName': 'internet explorer'} ] seleniumServerJar: './node_modules/gulp-protractor/node_modules/protractor/selenium/selenium-server-standalone-2.53.1.jar',framework: 'custom',
frameworkPath: 'node_modules/protractor-cucumber-framework',
specs: [ paths.features ],
jasmineNodeOpts: { showColors: true, defaultTimeoutInterval: 30000 },
cucumberOpts: { require: [paths.distFiles, paths.support],
format: "json"
}
};
typescript
var paths = require('build/paths');
exports.config = {
directConnect: true,
multiCapabilities: [ {'browserName': 'firefox'}, {'browserName': 'chrome'}, {'browserName': 'internet explorer'} ] seleniumServerJar: './node_modules/gulp-protractor/node_modules/protractor/selenium/selenium-server-standalone-2.53.1.jar',framework: 'custom',
frameworkPath: 'node_modules/protractor-cucumber-framework',
specs: [ paths.features ],
jasmineNodeOpts: { showColors: true, defaultTimeoutInterval: 30000 },
cucumberOpts: { require: [paths.distFiles, paths.support],
format: "json"
}
};

The above setup will run your end to end tests in Chrome, Firefox, and Internet Explorer 11 in parallel. You can remove browsers if for example it is okey to only test your tests in Chrome.

You can also replace the multiCapabilities property with the text below to only test in Chrome.

typescript
capabilities: { 'browserName': 'chrome' },
typescript
capabilities: { 'browserName': 'chrome' },

Extend your build\paths.js file once more, to include the new paths:

typescript
module.exports = {
dist: "dist/",
distFiles: "dist/**/*.js",
testResultJson: "./reports/cucumber-test-results.json",
e2eReports: "reports/e2e",
features: "features/**/*.feature",
support: "support/*.js"
};
typescript
module.exports = {
dist: "dist/",
distFiles: "dist/**/*.js",
testResultJson: "./reports/cucumber-test-results.json",
e2eReports: "reports/e2e",
features: "features/**/*.feature",
support: "support/*.js"
};

Setup create screenshot on failure #

It would be nice to see a screenshot of the current view if the test fails. The script below will add a screenshot to a failed step.

Create a new file named TakeScreenshot.js in the support folder with the following content:

typescript
module.exports = function TakeScreenshot() {
this.After(function (scenario, callback) {
if (scenario.isFailed()) {
browser
.takeScreenshot()
.then(function (png) {
var decodedImage = new Buffer(png.replace(/^data:image\/(png|gif|jpeg);base64,/,''), 'base64');scenario.attach(decodedImage, 'image/png');
callback();
});
} else {
callback();
}
});
};
typescript
module.exports = function TakeScreenshot() {
this.After(function (scenario, callback) {
if (scenario.isFailed()) {
browser
.takeScreenshot()
.then(function (png) {
var decodedImage = new Buffer(png.replace(/^data:image\/(png|gif|jpeg);base64,/,''), 'base64');scenario.attach(decodedImage, 'image/png');
callback();
});
} else {
callback();
}
});
};

Add another new file with the name jsonOutputHook.js (also in the support folder) and add the content:

typescript
var paths = require("../build/paths");
module.exports = function JsonOutputHook() {
var Cucumber = require('cucumber');
var JsonFormatter = Cucumber.Listener.JsonFormatter();
var fs = require('fs');
var path = require('path');
JsonFormatter.log = function (json) {
fs.writeFile(path.join(__dirname, '../'+ paths.testResultJson), json,
function (err) {
if (err) throw err; console.log('json file location: ' + path.join(__dirname, '../' + paths.testResultJson));
});
};
this.registerListener(JsonFormatter);
};
typescript
var paths = require("../build/paths");
module.exports = function JsonOutputHook() {
var Cucumber = require('cucumber');
var JsonFormatter = Cucumber.Listener.JsonFormatter();
var fs = require('fs');
var path = require('path');
JsonFormatter.log = function (json) {
fs.writeFile(path.join(__dirname, '../'+ paths.testResultJson), json,
function (err) {
if (err) throw err; console.log('json file location: ' + path.join(__dirname, '../' + paths.testResultJson));
});
};
this.registerListener(JsonFormatter);
};

When this is in place gulp e2e will run your end to end test and gulp e2e-report will generate a report based on the results from the last gulp e2e run.

Example of a failing scenario #

screenshot of failing report

Example of a feature #

A feature is defined in a .feature file and can have multiple scenarios defined in a language called Gherkin. Gherkin is a language that allows you to keep a definition of the business logic that can be shared with the stakeholders of the project.

A .feature file (stored in the features folder) can, for example, contain the following scenario:

gherkin
Feature: Create a new teamie In order to start a new teamie
As an team member that is the organizer of a teamie survey for my team I want to be able to create a teamie so that my team can improve
Background:
Given I received an link to create a new Teamie
And I opened the link in my browser
Scenario: Team name must be set
Given I don't enter a team name for my team
Then A error message should occur 'The team name cannot be empty'
gherkin
Feature: Create a new teamie In order to start a new teamie
As an team member that is the organizer of a teamie survey for my team I want to be able to create a teamie so that my team can improve
Background:
Given I received an link to create a new Teamie
And I opened the link in my browser
Scenario: Team name must be set
Given I don't enter a team name for my team
Then A error message should occur 'The team name cannot be empty'

To implement the logic behind the above story we implement a step definition (in the steps folder) and page objects (in the page-objects folder).

A page object is a reusable structure of the page itself. This allows us to write this logic once and use it in any step definition that requires it. Which means a page object is the object that contains all our Selenium like syntax to access objects on our page.

typescript
export default class NewTeamiePageObject {
public static setTeamName(name: string): webdriver.promise.Promise<void> {
return element(by.id("lblName")).sendKeys(name);
}
}
typescript
export default class NewTeamiePageObject {
public static setTeamName(name: string): webdriver.promise.Promise<void> {
return element(by.id("lblName")).sendKeys(name);
}
}

Next we have a steps file, this file will has the methods that will be called by CucumberJS if they match regex pattern defined in the decorator. And will in turn call the methods defined on our page object.

Below is the sample code to handle the step Given I don't enter a team name for my team. Each class that contains steps must be decorated with the @bindings decorator and each method must be decorated with the @given (or @when, @then) decorator.

CucumberJS will test all the regular expressions defined on the step definitions and execute the step definition if it’s a match with the step text.

As you can see below there is also a method with the regular expression /^I enter the team name '(.*)' for my team$/. This regex will handle strings that match I enter the team name 'team 1' for my team and supply the value team 1 to the method to make your code more flexible.

typescript
import { binding, given, then, when } from "cucumber-tsflow";
import { expect } from "chai";
import { newTeamiePageObject } from "./../../page-objects/NewTeamiePageObject";
@binding()
class NewTeamieSteps {
@given(/^I don't enter a team name for my team$/)
public GivenIDontEnterATeamNameForMyTeam (callback): void {
newTeamiePageObject.setTeamName("").then(callback);
}
@given(/^I enter the team name '(.*)' for my team$/)
public GivenEnterATeamNameForMyTeam (teamName, callback): void {
newTeamiePageObject.setTeamName(teamName).then(callback);
}
}
export = NewTeamieSteps;
typescript
import { binding, given, then, when } from "cucumber-tsflow";
import { expect } from "chai";
import { newTeamiePageObject } from "./../../page-objects/NewTeamiePageObject";
@binding()
class NewTeamieSteps {
@given(/^I don't enter a team name for my team$/)
public GivenIDontEnterATeamNameForMyTeam (callback): void {
newTeamiePageObject.setTeamName("").then(callback);
}
@given(/^I enter the team name '(.*)' for my team$/)
public GivenEnterATeamNameForMyTeam (teamName, callback): void {
newTeamiePageObject.setTeamName(teamName).then(callback);
}
}
export = NewTeamieSteps;

Note: the last line (the export) is required to be written in this way to get things to work, using export default class won't work :-(