22 June 2017 - on 

In a previous article I talked about how can we drive the development our AngularJS 4 application using BDD with Cucumber.

A frequent problem that we have to face when executing E2E tests using some browser automation tool is to setup the initial conditions required for each of the scenarios we are testing, specially when our application takes the data from a backend using, for example, a RESTful Web Service.

To solve this in AngularJS 1 we could inject a mock module using the browser.addMockModule method that Protactor offers in its API. Unfortunately this doesn't work since AngularJS 2.

In this article we are going to see how can we use MockServer to mock the responses that our application receives from the backend in the Cucumber scenarios of our AngularJS 4 application.

Sample application

The application is simply going to show a list of products when we navigate to /products. The list of products to show it's going to come from a RESTful Web Service.

We are interested in implement two scenarios:

  • When the Web Service returns an empty list of products, our application should show a message indicating that there is yet no available product.
  • When the Web Service returns a list of products, our application will show a list of the products.

As base project for our application we are going to use the project that we already configured to run the E2E testing with Cucumber in a previous article. 

git clone git@github.com:jagilpe/angular4-bdd-cucumber.git angular4-bdd-mock-backend
cd angular4-bdd-mock-backend
npm install

Refactor the application to add routing support

The base application we are using doesn't have the routing support enabled. Before we start thinking in the new feature we are going to refactor the application to add the routing support. The E2E tests will help us confirm that after the refactoring everything is still working as it should.

We run the E2E tests to be sure that we are in green area before we begin the refactoring.

npm run e2e

[...]
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!!"

1 scenario (1 passed)
4 steps (4 passed)
[...]

 Now we can start the refactoring. First we are going to move the home page logic to a new component HomeComponent.

// src/app/home/home.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-home-page',
  templateUrl: './home.component.html'
})
export class HomeComponent {
  name: string;
  greeting = 'Welcome!!';

  greet(): void {
    this.greeting = this.name !== null ? `Welcome ${this.name}!!` : 'Welcome!!';
  }
}
<!-- src/app/home/home.component.html -->

<div style="text-align:center">
  <h1 id="greeting">
    {{ greeting }}
  </h1>
  <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>
</div>

The AppComponent will now simply have a router-outlet element.

// 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 {
}
<!-- src/app/app.component.html -->

<router-outlet></router-outlet>

Now we simply add the HomeComponent to the main app module and enable the routing.

// 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';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';

const appRoutes: Routes = [
  { path: '',  component: HomeComponent }
];

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    RouterModule.forRoot(appRoutes)
  ],
  providers: [],
  entryComponents: [
    HomeComponent
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

With this the refactoring is finished. If we run the E2E tests again we can confirm that the behavior of the application hasn't changed.

Configure the E2E tests

Install required dependencies

The first thing we have to do is to install the Node module we will use to start and stop the MockServer. We also have to install the MockServer javascript client we are going to use to configure the different responses we need our mock backend to give.

npm install --save-dev mockserver-grunt mockserver-client

Configure the MockServer

We want to start the Mock Server before the execution of the E2E Scenarios and stop it after we have finished with them. For this we can use the beforeLaunch and afterLaunch hooks of Protractor. Edit the protractor.conf.js file to include the following:

# protractor.conf.js

var MOCKSERVER_PORT = 11080;

exports.config = {
  // ...
  beforeLaunch: () => {
    // ...
    let mockServer = require('mockserver-grunt');
    return mockServer.start_mockserver({ serverPort: MOCKSERVER_PORT });
  },
  afterLaunch: () => {
    // ...
    let mockServer = require('mockserver-grunt');
    return mockServer.stop_mockserver({ serverPort: MOCKSERVER_PORT });
  }
};

This will start the Mock Server in the port 11080 during the E2E tests. Now we have to make that our application connects to it instead of the real backend server.

Configure an E2E environment

We are going to define a new angular-cli environment for our E2E tests, so that we can define a different backend base url for the production, the development and the E2E environments.

Add a backendUrl property to our environments definitions that points to the real backends.

// src/environments/environment.ts

export const environment = {
  production: false,
  // Development Backend base url
  backendUrl: 'http://dev-api.example-backend.com'
};
// src/environments/environment.prod.ts

export const environment = {
  production: true,
  // Production Backend base url
  backendUrl: 'http://api.example-backend.com'
};

Add a new environment file for our E2E tests:

// src/environments/environment.e2e.ts

export const MOCKSERVER_PORT = 11080;

export const environment = {
  production: false,
  backendUrl: 'http://localhost:' + MOCKSERVER_PORT
};

Add the new environment in the angular-cli configuration

//.angular-cli.json

{
  // ...
  "apps": [
    {
      // ...
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts",
        "e2e": "environments/environment.e2e.ts"
      }
    }
  ]
  // ...
}

Add the environment to the e2e npm script.

// package.json

{
  // ...
  "scripts": {
    // ...
    "e2e": "ng e2e --environment=e2e",
    // ...
  },
  // ...
}

Now we can access this parameter from our AngularJS Module and we will get the right value depending on the environment. We create a new file that will have all the configurable parameters of the application.

// src/app/app-config.ts

import { environment } from '../environments/environment';
import { InjectionToken } from '@angular/core';

export interface AppConfig {
  apiEndpoint: string;
}

export const DEFAULT_APP_CONFIG: AppConfig = {
  apiEndpoint: environment.backendUrl
};

export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');

We inject this configuration in the main module.

// src/app/app.module.ts

// ...
import { APP_CONFIG, DEFAULT_APP_CONFIG } from './app-config';
// ...

@NgModule({
  // ...
  providers: [
    { provide: APP_CONFIG, useValue: DEFAULT_APP_CONFIG }
  ],
  // ...
})
export class AppModule { }

Write the tests specifications

Now we will write the specification we have to implement as scenarios in a new feature file called product-list.feature

# e2e/features/product-list.feature
Feature: List the shop's products
  As a user of the shop
  I should get a list of products

  Scenario: An empty products list
    Given the shop has no product
    When I go to the product list page
    Then I should get an empty list message

  @pending
  Scenario: Get a list of products
    Given the shop has three products
    When I go to the product list page
    Then I should not get an empty list message
    And the product list has:
      | name      |
      | Product 1 |
      | Product 2 |
      | Product 3 |

We are going to start implementing the first scenario, so we tag the second scenario of our feature as pending. Don't forget to configure cucumber to exclude the scenarios marked with pending.

// protractor.conf.js

exports.config = {
  // ...
  cucumberOpts: {
    // ...
    tags: [ "~@pending" ],
    // ...
  },
  // ...
};

Implement the first scenario

The first thing is to write the steps required by the first scenario.

// e2e/step-definitions/product-list.steps.ts

import { defineSupportCode } from 'cucumber';
import { ProductListPageObject } from '../pages/product-list.po';
import { AppMockBackend } from '../util/mock-backend';
import * as chai from 'chai';
import * as chaiAsPromised from 'chai-as-promised';

defineSupportCode(({ Before, Given, When, Then }) => {

  const productListPage = new ProductListPageObject();
  const mockBackend = new AppMockBackend();

  Before(() => {
    chai.should();
    chai.use(chaiAsPromised);
    return mockBackend.reset();
  });

  Given(/^the shop has (.*)$/, (fixture) => {
    mockBackend.setFixture(fixture);
  });

  When(/^I go to the product list page$/, () => {
    return productListPage.get();
  });

  Then(/^I should get an empty list message$/, (callback) => {
    productListPage.hasEmptyListMessage().should.eventually.be.true.notify(callback);
  });
});

To keep the code of the step definitions clear we have used a PageObject for the Product List page to encapsulate all its details.

// e2e/pages/product-list.po.ts

import { browser, by, element, promise } from 'protractor';

export class ProductListPageObject {

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

  hasEmptyListMessage(): promise.Promise<boolean> {
    return element(by.css('#empty-list-message')).isPresent();
  }
}

We have also used a helper class that will let us interact with the MockServer.

// e2e/util/mock-backend.ts

import { MOCKSERVER_PORT } from '../../src/environments/environment.e2e';

const mockServer = require('mockserver-client');

export class AppMockBackend {

  private mockServerClient;

  constructor() {
    this.mockServerClient = mockServer.mockServerClient('localhost', MOCKSERVER_PORT);
  }

  reset(): Promise<void> {
    this.mockServerClient.setDefaultHeaders([
      {'name': 'Content-Type', 'values': ['application/json; charset=utf-8']},
      {'name': 'Cache-Control', 'values': ['no-cache, no-store']},
      {'name': 'Access-Control-Allow-Origin', 'values': '*'}
    ]);
    return this.mockServerClient.reset();
  }

  setFixture(fixture: string): void {
    switch (fixture) {
      case 'no product':
        this.mockServerClient.mockSimpleResponse('/products', [], 200);
        break;
    }
  }
}

The reset method sets the default headers of the responses of the mock server, and then resets all the expectations, so that we can start each scenario with a clean state.

The setFixture method is where we configure the responses that the MockServer is expected to return for a determined scenario. Here we use the mockSimpleResponse method to configure a really simple interaction with the backend. You can read the MockServer javascript client reference to learn how to create more complex scenarios.

Now we have a complete definition of our first scenario and we can implement it.

Add a new component for the product list page.

// src/app/products/product-list.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html'
})
export class ProductListComponent {
}
<!-- src/app/products/product-list.component.html -->

<div id="empty-list-message">
  No products found
</div>

Finally we add the new route and the new component in the main module

// src/app/app.module.ts

// ...
import { ProductListComponent } from './products/product-list.component';
// ...

const appRoutes: Routes = [
  // ...
  { path: 'products',  component: ProductListComponent }
];

@NgModule({
  declarations: [
    // ...
    ProductListComponent
  ],
  // ... 
  entryComponents: [
    // ...
    ProductListComponent
  ],
  // ...
})
export class AppModule { }

As you can see the ProductListComponent doesn't do anything more than show the empty list message that the test scenario is waiting for, but with it we make the first scenario pass.

npm run e2e

[...]
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!!"

Feature: List the shop's products

    As a user of the shop
    I should get a list of products

  Scenario: An empty products list
  ✔ Given the shop has no product
  ✔ When I go to the product list page
  ✔ Then I should get an empty list message

2 scenarios (2 passed)
7 steps (7 passed)
[...]

Implement the second scenario

Now we are going to implement the second scenario of our product list feature. The first thing we are going to make is to remove de pending tag of the scenario and to write (and rewrite) the step definitions required to run the scenario.

We are going to change the step that checks the empty list message to add an optional negation of the check. We also add a new step to check the elements of the list.

// e2e/step-definitions/product-list.steps.ts

defineSupportCode(({ Before, Given, When, Then }) => {
  // ...
  Then(/^I should( not)? get an empty list message$/, (negate, callback) => {
    if (!!negate) {
      productListPage.hasEmptyListMessage().should.eventually.be.false.notify(callback);
    } else {
      productListPage.hasEmptyListMessage().should.eventually.be.true.notify(callback);
    }
  });

  Then(/^the product list has:$/, (expectedProducts, callback) => {
    productListPage.getProducts().should.eventually.deep.equal(expectedProducts.hashes()).notify(callback);
  });
});

We add a new method to the product list page object to get the list of products of the page.

// e2e/pages/product-list.po.ts

// ...

export class ProductListPageObject {
  // ...

  getProducts(): promise.Promise<string[]> {
    return element.all(by.css('ul#products-list li.product-item'))
      .map(productListItem => ({ name: productListItem.getText() }));
  }
}

And finally we add the new fixture to the mock backend.

// e2e/util/mock-backend.ts

// ...
export class AppMockBackend {

  // ...
  setFixture(fixture: string): void {
    switch (fixture) {
      case 'no product':
        this.mockServerClient.mockSimpleResponse('/products', [], 200);
        break;
      case 'three products':
        this.mockServerClient.mockSimpleResponse('/products', [
          { id: 1, name: 'Product 1'},
          { id: 2, name: 'Product 2'},
          { id: 3, name: 'Product 3'}
        ], 200);
        break;
    }
  }
}

We are going to use underscore in the implementation of this scenario so we don't have to forget to install it.

npm install --save underscore @types/underscore

Now we can go on implementing the scenario. First the ProductListComponent.

// src/app/products/product-list.component.ts

import { Component, Inject, OnInit } from '@angular/core';
import { Http, Response } from '@angular/http';
import * as _ from 'underscore';
import { APP_CONFIG, AppConfig } from '../app-config';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html'
})
export class ProductListComponent implements OnInit {

  products: { name: string }[];

  constructor(private http: Http, @Inject(APP_CONFIG) private appConfig: AppConfig) {}

  public ngOnInit(): void {
    const url = `${this.appConfig.apiEndpoint}/products`;

    // Get the list of products
    this.http.get(url)
      .subscribe((response: Response) => this.products = response.json());
  }

  get hasProducts(): boolean {
    return this.products && !_.isEmpty(this.products);
  }
}
<!-- src/app/products/product-list.component.html -->

<div id="empty-list-message" *ngIf="!hasProducts">
  No products found
</div>
<ul id="products-list" *ngIf="hasProducts">
  <li class="product-item" *ngFor="let product of products">{{ product.name }}</li>
</ul>

Import the HttpModule.

// src/app/app.module.ts

// ...
import { HttpModule } from '@angular/http';

// ...

@NgModule({
  // ...
  imports: [
    // ...
    HttpModule
  ],
  // ...
})
export class AppModule { }

With this we have finished implementing the two scenarios that we have defined for our new product list feature. If we run the E2E tests once again we'll see that all scenarios are now green.

npm run e2e

[...]
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!!"

Feature: List the shop's products

    As a user of the shop
    I should get a list of products

  Scenario: An empty products list
  ✔ Given the shop has no product
  ✔ When I go to the product list page
  ✔ Then I should get an empty list message

  Scenario: Get a list of products
  ✔ Given the shop has three products
  ✔ When I go to the product list page
  ✔ Then I should not get an empty list message
  ✔ And the product list has:
      | name      |
      | Product 1 |
      | Product 2 |
      | Product 3 |

3 scenarios (3 passed)
11 steps (11 passed)
[...]