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
(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
Install Following Dependencies
"dependencies": {
"axios": "^0.20.0",
},
"devDependencies": {
"cucumber": "^6.0.5",
"cucumber-html-reporter": "^5.2.0"
}
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 |
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;
})
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'"
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.
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
}
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);
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)
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 ..."