BDD Style API Tests using Cypress and Cucumber
sam-nash
Posted on February 23, 2023
I am just here to beat Medium(the formatting sucks, you cant read more than 3 stories...).
I like BDD(It has a great syntax which anybody can understand). Especially testers, business users, product team who aren't technical.
But BDD shouldn't be used to automate end users' acceptance testing. The whole idea of acceptance testing is to let the customer(end user) look at, feel and use the application like they normally do in a real world, but not automate their behaviour.
BDD might be useful for Product Owners/Business Analysts, but again, is it worth the effort of a automation engineer to implement BDD for those 1 or 2 people?
And especially for API Testing, I firmly believe BDD is a NO. I think a PO or BA can see the developer or tester demo it to them in the Sprint review.
BDD is GOOD but I think it is an overkill for API Testing(Or any backend tests). But we'll still explore how to do that today.
Recently, I have become a great fan of cypress.io and started to use it for almost everything.
Cypress is an open-source end-to-end testing framework used to test web applications. It provides a fast, reliable, and easy-to-use testing experience, with an intuitive API and an extensive set of built-in commands.
Cypress is great. In the last 1 year there have been 3 major version upgrades, easy to use, open source, community support is increasing.
Cucumber is a popular tool used for behavior-driven development (BDD). It allows you to define test cases in a human-readable format, which can be easily understood by non-technical stakeholders. By using Cucumber with Cypress, you can create tests that are easy to write, easy to read, and easy to maintain.
Overview of the Git Repo
The git repo contains a sample project that demonstrates how to use Cypress with Cucumber.
The tests are organized into feature files, which are meant to describe different aspects of the weather api. For example, the stations.feature
file contains test cases related to the functionality of weather station creation, & we could add another solarRadion.feature file that can contain test cases related to the Solar Radiation API.
Each feature file contains one or more scenarios, which describe a specific test case. Scenarios are written in Gherkin syntax, which is a human-readable format that allows you to describe the expected behavior of the web application.
Our API under Test
We'll make use of the OpenWeatherMap Stations API for our tests.
The API exposes the following methods :
POST(to register a stations)
PUT(to update the information about a station)
GET(to retrieve all the stations or a specific station using the stationId)
DELETE(a station that was created by the current user)
Generate an API Key
The Open Weather uses an API Key for Authentication.
To generate and use the apiKey in your tests :
Register online at OpenWeather and generate an api key.
Set Up and Install
If you want to skip all the step-by-step installations and set up, then I recommend that you clone this repo.
Once cloned, cd to the cloned directory and perform
npm install
Browse through some of the tests that I wrote already to perform POST + GET operations and to verify the response.
Cypress configuration
The default cypress configuration
cypress.config.js
has the basics that we need to test the API + Reporting.Additionally create a
cypress.env.json
file at the root of your project folder & update the file like below. ReplaceapiKey
with the key that was generated previously.
example :
{ "appid": "<apiKey>" }
NOTE : Let this cypress.env.json
file be local to your machine & gitignored. It is a best practice to generate this dynamically or store this as an env var in the CI server.
Cypress custom commands
As we might make use of cy.request() multiple times with different methods, I've created custom commands in (commands.js)[cypress/support/commands.js]
This makes it user friendly(for users who don't want to be too bothered with the technical details of how to make requests).
Better readability of code.
To Run the tests
From your proj dir: type this command on the console and hit return.
npm run cucumberTest
You should see the tests running and pass. You should also see the test results printed on the console.
If you'd like to see a html report, navigate to cypress/reports and open the index.html
Under the Hood
So what actually happened here ?
The Tests rely on two files - the Feature
and the spec or implementation
The Feature file is the file which contains the Feature & the associated scenarios that are being tested in the Given
When
Then
And
format.
The Spec file is the implementation of the feature file. This is where Cypress code resides that makes API calls and uses assertions to verify the response received.
The Given, When, Then & And sections in the Feature file have matching Given, When, Then & And code blocks in the Spec file.
Feature = 'saying'
Spec = 'doing'
Construct - Feature/Test Organization
a. All Feature files must be created with the file name convention name.feature under the cypress/e2e folder [example: stations.feature]
b. Create a corresponding folder under e2e that has the same name as the feature name above(without '.feature') [example : stations]
c. Create/add your cypress spec/feature implementation under this folder [example : /e2e/stations/apiTest.cy.js]
d. To add your own scenarios and tests, edit the feature file and edit/add more API test scenarios using the Given, When, Then & And construct.
e. And then add the matching tests to the apiTest.js
Approaches
To demonstrate API Testing with Cypress & Cucumber, I have added tests that make use of two approaches as described below.
- DATATABLE - Feature is in the stations.feature file that has the test data in a tabular form(to test multiple data combos) & the cypress spec is in the e2e/stations/apiTest.js which has the implementation. The cypress spec loops through the data to POST and performs a response code verification & stores each stationId in an array. It then also sends a GET request for each stationId & verifies the response to check if it matches the original request that was previously sent in the POST call.
For any Scenario :
GIVEN
- In the Given section of the feature file we pass a table as the data.
Given I have the following station data to post:
| external_id | name | latitude | longitude | altitude |
| DEMO_TEST001 | Team Demo Test Station 001 | 33.33 | -122.43 | 222 |
| DEMO_TEST002 | Team Demo Test Station 002 | 44.44 | -122.44 | 111 |
- In the Given section of the spec(implementation) file, We pass the above datatable as a parameter & then make use of the Cucumber method:
hashes()
that belongs to the class:dataTable
to return an array of objects & then convert this datatable into a Map.
Given('I have the following station data to post:', (dataTable) => {
// Convert the data table to an array of objects with header keys
requestData = dataTable.hashes().map((row)
- We then convert string representation of numbers and floats to their respective types. If we dont do this the numbers and float values will be converted to strings.(We cant send latitiude as a string to the API).
Object.keys(row).forEach((key) => {
const value = row[key];
if (!isNaN(value)) {
if (value.includes('.')) {
row[key] = parseFloat(value);
} else {
row[key] = parseInt(value, 10);
}
}
});
- Then we wrap this array of objects as an alias for a later use.
cy.wrap(requestData).as('requestData');
WHEN
- In the feature file we specify the expected outcome of the POST call by specifying the http status code as a number.
When I post the station data request body to the Create Stations API, then I receive a response status code 201 and an unique alphanumeric stationId in the response
- In the spec file, we make a POST call using the cypress command we developed for this(the command basically sends a cy.request() with the required parameters like method, requestbody, apiKey).
When(
'I post the station data request body to the Create Stations API, then I receive a response status code {int} and an unique alphanumeric stationId in the response',
(statusCode) => {
cy.get('@requestData').each((request) => {
cy.createStation(apiKey, request).then((response) => {
//create an alias that stores the response received from the api.
expect(response.status).to.eq(statusCode); //verify the http status code
expect(response.body.ID).to.match(/^[a-z0-9]+$/i);
//store the stationId for later use to retrive the station for verification
stationIds.push(response.body.ID);
});
});
}
);
We then perform assertions on the statusCode that was passed as parameter to When.
Note that we passed the actual status code as 201 in the feature but in the implementation, we specified it as {int}. This is to tell the spec what to read from this position from the feature file.We then store the
stationIds
for a later use to make the GET call.
THEN
- Finally, in the Then section we make another API GET call for each station that was previously extracted and stored in an array & perform the required assertions.
Then(
'When the unique stationId is queried using a GET request, the api returns a {int} status code',
(statusCode) => {
//Call the GET API with the stationId created using the POST Request & store its response in an alias
stationIds.forEach((stationId) => {
cy.getStation(stationId).then((response) => {
expect(response.status).to.eq(statusCode);
- Without DATA TABLE - Feature is in the apiTests.feature & its implementation in the e2e/apiTests/apiTests.js. Here the scenarios have been separated for each POST Request(1 per each test data) and corresponding GET requests verify the data.
The ONLY intention to separate it this way is to demonstrate that an api request body can be added in the scenario's Given or When.
When it comes to reporting, in the former approach, the test report displays the various tests(for each test data combo) under the same scenario and in the latter, the report displays the various tests/scenarios separately.
Happy Testing!
Posted on February 23, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.