DEV Community

Cover image for REST API Testing With CucumberJs (BDD)
Yogesh Manware
Yogesh Manware

Posted on • Edited on

REST API Testing With CucumberJs (BDD)

Introduction

BDD is very powerful tool for both non-technical and technical people.

In this article, I will demonstrate how to set up and run Cucumber, to test REST APIs.

What is BDD really?

BDD is a short for Behaviour Driven Development
BDD is a way for software teams to work that closes the gap between business and technical people by:

  • Encouraging collaboration across roles to build shared understanding of the problem to be solved
  • Working in rapid, small iterations to increase feedback and the flow of value
  • Producing system documentation that is automatically checked against the system’s behaviour

We do this by focusing collaborative work around concrete, real-world examples that illustrate how we want the system to behave. We use those examples to guide us from concept through to implementation.

What is Cucumber?

Cucumber is a tool that supports Behaviour-Drive Development(BDD). Cucumber reads executable specifications written in plain text and validates that the software does what those specifications say. The specifications consists of multiple examples, or scenarios. For example:

Scenario Outline: create a contact
    Given A contact <request>
    When I send POST request to /directory
    Then I get response code 201
Enter fullscreen mode Exit fullscreen mode

(This scenario is written using Gherkin Grammar)
Each scenario is a list of steps for Cucumber to work through. Cucumber verifies that the software conforms with the specification and generates a report indicating ✅ success or ❌ failure for each scenario.

What is Gherkin?

Gherkin is a set of grammar rules that makes plain text structured enough for Cucumber to understand. Gherkin documents are stored in .feature text files and are typically versioned in source control alongside the software.

How Gherkin's .feature file glues to your code?

We write step definitions for each step from Gherkin's feature file. Step definitions connect Gherkin steps to programming code. A step definition carries out the action that should be performed by the step. So step definitions hard-wire the specification to the implementation.

Feature

A feature is a group of related scenarios. As such, it will test many related things in your application. Ideally the features in the Gherkin files will closely map on to the Features in the application — hence the name
Scenarios are then comprised of steps, which are ordered in a specific manner:

Given – These steps are used to set up the initial state before you do your test
When – These steps are the actual test that is to be executed
Then – These steps are used to assert on the outcome of the test

Example

I have created a simple REST API to manage a directory. I can create contact, modify it, read it and delete a contact. I have written BDD tests to make sure all features work as designed.

Setup NodeJs Project

npm init
Enter fullscreen mode Exit fullscreen mode

Install Following Dependencies

 "dependencies": {
    "axios": "^0.20.0",
  },
  "devDependencies": {
    "cucumber": "^6.0.5",
    "cucumber-html-reporter": "^5.2.0"
  }
Enter fullscreen mode Exit fullscreen mode

Create directory.feature file at src/features

@directory-service
Feature: Directory Service
  In order to manage directory
  As a developer
  I want to make sure CRUD operations through REST API works fine

  Scenario Outline: create a contact
    Given A contact <request>
    When I send POST request to /directory
    Then I get response code 201

    Examples:
      | request                                                                                          
      | {"id":99,"name":"Dwayne Klocko","email":"Rene30@hotmail.com","phoneNumber":"1-876-420-9890"}          |
      | {"id":7,"name":"Ian Weimann DVM","email":"Euna_Bergstrom@hotmail.com","phoneNumber":"(297) 962-1879"} |

  Scenario Outline: modify contact
    Given The contact with <id> exist
    When I send PATCH request with a <secondaryPhoneNumber> to /directory
    Then I get response code 200

    Examples:
      | id | secondaryPhoneNumber                       |
      | 99 | {"secondaryPhoneNumber": "(914) 249-3519"} |
      | 7  | {"secondaryPhoneNumber": "788.323.7782"}   |

  Scenario Outline: get contact
    Given The contact with <id> exist
    When I send GET request to /directory
    Then I receive <response>

    Examples:
      | id | response                                      |
      | 99 | {"id":99,"name":"Dwayne Klocko","email":"Rene30@hotmail.com","phoneNumber":"1-876-420-9890","secondaryPhoneNumber": "(914) 249-3519"}         |
      | 7  | {"id":7,"name":"Ian Weimann DVM","email":"Euna_Bergstrom@hotmail.com","phoneNumber":"(297) 962-1879", "secondaryPhoneNumber": "788.323.7782"} |

  Scenario Outline: delete contact
    Given The contact with <id> exist
    When I send DELETE request to /directory
    Then I get response code 200

    Examples:
      | id |
      | 99 |
      | 7  |
Enter fullscreen mode Exit fullscreen mode

Create directory.js in src/steps

const {Given, When, Then, AfterAll, After} = require('cucumber');
const assert = require('assert').strict
const restHelper = require('./../util/restHelper');

Given('A contact {}', function (request) {
    this.context['request'] = JSON.parse(request);
});

When('I send POST request to {}', async function (path) {
    this.context['response'] = await restHelper.postData(`${process.env.SERVICE_URL}${path}`, this.context['request']);
})

Then('I get response code {int}', async function (code) {
    assert.equal(this.context['response'].status, code);
});

When('I send PATCH request with a {} to {}', async function (phoneNumberPayload, path) {
    const response = await restHelper.patchData(`${process.env.SERVICE_URL}${path}/${this.context['id']}`, JSON.parse(phoneNumberPayload));
    this.context['response'] = response;
})

Given('The contact with {int} exist', async function (id) {
    this.context['id'] = id;
})

When('I send GET request to {}', async function (path) {
    const response = await restHelper.getData(`${process.env.SERVICE_URL}${path}/${this.context['id']}`);
    this.context['response'] = response;
})

Then(/^I receive (.*)$/, async function (expectedResponse) {
    assert.deepEqual(this.context['response'].data, JSON.parse(expectedResponse));
})

When('I send DELETE request to {}', async function (path) {
    const response = await restHelper.deleteData(`${process.env.SERVICE_URL}${path}/${this.context['id']}`);
    this.context['response'] = response;
})

Enter fullscreen mode Exit fullscreen mode

Create a service that does actual REST calls

You can use any http client, I used axios.

To run the test and generate report

npm i
"./node_modules/.bin/cucumber-js -f json:cucumber.json src/features/ -r src/steps/ --tags '@directory-service'"
Enter fullscreen mode Exit fullscreen mode

In this command, parallel is used to run three scenarios concurrently.

That's all. I mean that is the gist of BDD with Cucumber and Gherkin.

Here is a sample cucumber report.
Alt Text

Sharing Data Between Steps

You would most likely need to share data between steps. Cucumber provides an isolated context for each scenario, exposed to the hooks and steps as this, known as World. The default world constructor is:

function World({ attach, log, parameters }) {
  this.attach = attach
  this.log = log
  this.parameters = parameters
}
Enter fullscreen mode Exit fullscreen mode

Note: you must not use anonymous functions in steps if you want to use World in steps.

const {setWorldConstructor} = require("cucumber");

if (!process.env.DIRECTORY_SERVICE_URL) {
    require('dotenv-flow').config();
}

class CustomWorld {
    constructor({parameters}) {
        this.context = {};
    }
}
setWorldConstructor(CustomWorld);
Enter fullscreen mode Exit fullscreen mode

Following are some handy libraries that I used during this demo.

.env file

I have used dotenv-flow npm to store environment specific variables.
Refer: https://github.com/kerimdzhanov/dotenv-flow

Setup Mock REST API

I have setup mock REST API using json server npm.
Refer: https://github.com/typicode/json-server

For Cucumberjs - https://github.com/cucumber/cucumber-js

Source Code - https://github.com/ynmanware/nodejs-bdd/tree/v1.0

In summary, BDD sets up ground for collaboration from all stakeholders. Using tags, you can run different set of BDD suits for DEV, SIT, UAT and even PROD through build pipelines. This setup could be really effective with CI/CD practice, it could speed up development and deployment cycle while maintaining the basic quality checks in place.

Top comments (1)

Collapse
 
rogersdevelopmentservices profile image
Matthew Shane Rogers • Edited

Hello Yogesh,
I'm trying to understand your script command, in
"-f json:cucumber.json src/features/ -r src/steps/"
Does the "-f" mention where to output cucumber.json files write to?
Does the src/features -r src/steps mean run the scripts found in the step definitions found in the steps folder for the feature files found in the features folder? Or does "-r" mean something else?

I ask because I'm trying to run feature files from two different groups of tags on windows with
"cucumber-js -f json:cucumberJS/test-cucumber.json src/features/ -r src/steps/ --tags \"@Tag1 or @Tag2\""
And I get: "Error: ENOENT: no such file or directory, open cucmber ..."