15 June 2017 - on 

TL;DR: Here you can find an AngularJS project that is already configured to start using BDD to develop your application. If you're interested in how it works, keep reading.

In this article I'm not going to talk about how to build a web application using AngularJS or what it is BDD or how to use Cucumber. This is a hands on example about how can we apply BDD in the development of an Angular JS.

Create the AngularJS project

For this project we are going to use angular-cli to develop the application. Please read the Angular CLI documentation about how to install it in your system.

To create the project, simply run:

ng new angular-bdd

Configure Cucumber as E2E testing framework

First install the required dependencies.

npm install --save-dev cucumber protractor-cucumber-framework chai chai-as-promised @types/chai @types/cucumber @types/chai-as-promised

Change the Protractor configuration to use Cucumber.js instead of jasmine to run the E2E tests. Edit protractor.conf.js:

// protractor.conf.js

exports.config = {
  allScriptsTimeout: 11000,
  specs: [
    './e2e/features/**/*.feature'
  ],
  capabilities: {
    'browserName': 'chrome'
  },
  directConnect: true,
  baseUrl: 'http://localhost:4200/',
  framework: 'custom',
  frameworkPath: require.resolve('protractor-cucumber-framework'),
  cucumberOpts: {
    require: ['./e2e/**/*.ts'],
    tags: [],
    strict: true,
    format: ["progress"],
    dryRun: false,
    compiler: [ 'ts:ts-node']
  },
  onPrepare: function() {
    browser.manage().window().maximize(); // maximize the browser before executing the feature files
  },
  beforeLaunch: () => {
    require('ts-node').register({
      project: 'e2e/tsconfig.e2e.json'
    });
  }
};

Basically we have changed set the framework as custom, setting the framework path to point to protractor-cucumber-framework. Another change we have done is replace the jasmine based specs with the cucumber features. Finally we have included some options for Cucumber.

Write the tests specifications

Feature files

In Cucumber we write the specification that our software should meet in .feature files written in a language called Gherkin. To know more about how this features are written you can refer to the Cucumber reference documentation.

First we are going to delete the old jasmine specifications from the e2e directory.

rm -f e2e/app.*.ts

We create a directory features in e2e, and create our first Cucumber feature file app.feature.

# e2e/features/app.feature
Feature: Greet the user
  As a user of the page
  I should get a customized greet

  Scenario: Get a customized greet
    Given I am on the Home page
    When I write "Tom" in the "name" input
    And I click on "greet" button
    Then the greeting should be "Welcome Tom!!"

The Feature section is simply a description of the feature we are testing in this file. A determined feature can have one or more scenarios that have to be implemented to consider the feature complete.

A Scenario is a concrete example that illustrates a business rule. It's composed by a list of steps. Cucumber will execute each of the steps to check if the Scenario is correctly implemented.

Cucumber doesn't know out-of-the-box how to execute the steps of your scenarios. You have to write a small piece of code, called Step Definition, that is used by Cucumber to translate plain text Gherkin steps into actions that will interact with the system.

If we execute now the e2e test using angular-cli we will get (the output has been shortened):

npm run e2e
** NG Live Development Server is listening on localhost:49152, open your browser on http://localhost:49152 **
(node:4166) [DEP0022] DeprecationWarning: os.tmpDir() is deprecated. Use os.tmpdir() instead.
Hash: caa43d4d3e159223ede3
Time: 15456ms
...
Warnings:

1) Scenario: Get a customized greet # e2e/features/app.feature:5
   ? Given I am on the Home page
       Undefined. Implement with the following snippet:

         Given('I am on the Home page', function (callback) {
           // Write code here that turns the phrase above into concrete actions
           callback(null, 'pending');
         });
       
   ? When I write "Tom" in the "name" input
       Undefined. Implement with the following snippet:

         When('I write {string} in the {string} input', function (string, string2, callback) {
           // Write code here that turns the phrase above into concrete actions
           callback(null, 'pending');
         });
       
   ? And I click on "greet" button
       Undefined. Implement with the following snippet:

         When('I click on {string} button', function (string, callback) {
           // Write code here that turns the phrase above into concrete actions
           callback(null, 'pending');
         });
       
   ? Then the greeting should be "Welcome Tom!!"
       Undefined. Implement with the following snippet:

         Then('the greeting should be {string}', function (string, callback) {
           // Write code here that turns the phrase above into concrete actions
           callback(null, 'pending');
         });
       

1 scenario (1 undefined)
4 steps (4 undefined)
0m00.000s
[09:56:21] I/launcher - 0 instance(s) of WebDriver still running
[09:56:21] I/launcher - chrome #01 failed 1 test(s)
[09:56:21] I/launcher - overall: 1 failed spec(s)
[09:56:21] E/launcher - Process exited with error code 1

The tests fails telling that it have found an scenario with four steps, marking it all as undefined. It gives you also a code snippet in javascript for each of the step as a base implementation.

Step definitions

We are going to write our Step Definitions in a directory called step-definitions in e2e using typescript. We start by adding the file steps.ts.

// e2e/step-definitions/steps.ts
import { HomePageObject } from "../pages/home.po";
import { Given, Then, When } from 'cucumber';

const chai = require('chai').use(require('chai-as-promised'));
const expect = chai.expect;

let homePage = new HomePageObject();

Given('I am on the Home page', (callback) => {
  homePage.get().then(() => callback());
});

When('I write {string} in the {string} input', (value, input, callback) => {
  homePage.setInput(input, value).then(() => callback());
});

When('I click on {string} button', (button, callback) => {
  homePage.clickButton(button).then(() => callback());
});

Then('the greeting should be {string}', (greeting, callback) => {
  greeting = greeting.replace(/['"]+/g, '');
  homePage.getGreeting().then(pageGreeting => {
    expect(pageGreeting).to.equal(greeting);
    callback();
  });
});

A common pattern when writing end to end test is to use Page Objects to keep out test cleaner by encapsulating the information about the elements of the page. In this case we have written a page object for the home page of our application.

//e2e/pages/home.po.ts
import { browser, by, element, promise } from "protractor";

export class HomePageObject {

  get(): promise.Promise<void> {
    return browser.get('/');
  }

  setInput(inputName: string, value: string): promise.Promise<void> {
    return element(by.css(`input[name="${inputName}"]`)).sendKeys(value);
  }

  getGreeting(): promise.Promise<string> {
    return element(by.css('#greeting')).getText();
  }

  clickButton(buttonName: string): promise.Promise<void> {
    return element(by.css(`button#${buttonName}`)).click();
  }
}

Now we have our first scenario defined and in a failing state. Now it's time to implement it, so that our scenario test passes.

Implement the scenario

The only thing left now is to write the code that makes this scenario pass.

Add the FormsModule to the app module.

#src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { FormsModule } from "@angular/forms";

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Write the template of our home page.

#src/app/app.component.html
<div style="text-align:center">
  <h1 id="greeting">
    {{ greeting }}
  </h1>
</div>
<div class="form-group">
  <label for="name">What's your name?:</label>
  <input id="name" name="name" [(ngModel)]="name"/>
</div>
<button id="greet" (click)="greet()">Greet me!</button>

And write the logic in the component.

#src/app/app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  name: string;
  greeting = 'Welcome!!';

  greet(): void {
    this.greeting = this.name !== null ? `Welcome ${this.name}!!` : 'Welcome!!';
  }
}

If we run now the E2E tests once again we'll see that the scenario is now passing.

npm run e2e
** NG Live Development Server is listening on localhost:49152, open your browser on http://localhost:49152 **
...
....

1 scenario (1 passed)
4 steps (4 passed)
0m01.591s
...

Continuous integration support

If we want to run the E2E tests in a continuous integration pipeline we run the test using chrome using the headless option (this is valid also for the unit tests, but this is another story)

#protractor.conf.js
exports.config = {
  //...
  capabilities: {
    'browserName': 'chrome',
    'chromeOptions': {
      'args': [ "--headless", "--disable-gpu", "--window-size=800,600" ]
    }
  },
  //...
};

Before we run the test we have to update the webdriver. For this we have to include a pre e2e script in npm.

// package.json
{
  //...
  "scripts": {
    //...
    "pree2e": "webdriver-manager update"
  },
  //...
}

You can see the resulting code in the GitHub repo https://github.com/jagilpe/angular-bdd-cucumber