Demo: API Testing Using Squish

Testing is an important part of the development of a software, the idea being that the more thorough the tests, the higher the chances are of discovering code defects or bugs. While Squish focuses on GUI testing, it can also be used for other kinds of testing, including what we would like to discuss in this article: API testing.

An API, or Application Programming Interface, is a GUI-less element of an application whose purpose is usually to provide data to other parts of the software (commonly the UI), to update the data storage/state of the software, and more. Following this description we can see that API testing is a regrouping of different kinds of testing:

  • Functional testing, validating the behavior against a set of expectations
  • Security testing, validating authentication, access control and/or encryption
  • Load testing, validating functionality and performance under load
  • And more

Given most of these examples are good automation candidates, we want to present an example of how this can be done with Squish. We will limit ourself to a simple functional test of a small REST API but the process can easily be extended to other kinds of testing or other types of APIs (SOAP, to name another). More about REST APIs can be found here.

The API

Since the focus is not on the API implementation but rather on how to test it with Squish, we will make the choice of simplicity and use an already available simple REST API that can be accessed here: https://reqres.in/. Changes made via REST calls on the different endpoints are not persistent, the interesting part for us is sending/receiving possible real world data.

The Tests

Squish supports writing test cases in two forms: through pure scripting or through what is known as Behavior-Driven Development (BDD) tests. BDD tests are written using the Gherkin syntax, which allows developing a complex test case while maintaining its comprehensibility to non-technical users. More info on BDD and Gherkin can be found here and here, respectively.

As pointed out above, we want to showcase how our simple example of functional testing of the API would look like assuming one of the two approaches:

  • Business, or non-technical, users authoring a test case
  • Technical users authoring a test case

We'll leave it to the reader to decide which approach he or she is more comfortable with, or both. The list of BDD scenarios we propose is not an exhaustive list of all possible scenarios, and a number of corner cases are not covered for the sake of simplicity.

Business Approach

The business, or non-technical approach, typically bases the tests on use case, leading to more verbose and understandable scenarios.

Tests written or designed by non-technical users are usually extracted from the product requirements or a common use case. This leads to expressive scenarios and steps that contain, sometimes, almost no technical information. The downside to this approach is that there is a need for clear communication between non-technical users' intent with each scenario, and the technical users actually implementing the steps based on the expectations.

Here is an example feature file:

Feature: Testing a REST API

Scenario: User registration is unsuccessful without password
When user sends a registration request without password
Then the server returns an error status code
And a payload containing an error message

Scenario: User can create another user
When user sends a user creation request
Then the server returns a success status code
And a payload containing user data

Scenario: User can browse the list of all available colors and delete one of them
Given the user has fetched the list of all colors
When user sends a delete request for one of them
Then the server returns a success status code

And the corresponding steps file:

import * as names from 'names.js';

When("user sends a registration request without password", function(context) {
var client = new XMLHttpRequest();
client.open('POST', 'https://reqres.in/api/register', false);
client.setRequestHeader("Content-Type", "application/json");
var data = '{"email": "tester@froglogic.com"}';
client.send(data);
context.userData["sentData"] = data;
context.userData["status"] = client.status;
context.userData["response"] = JSON.parse(client.response);
});

Then("the server returns an error status code", function(context) {
test.verify( context.userData["status"] == 400 );
});

Then("a payload containing an error message", function(context) {
var response = context.userData["response"];
test.verify( response["error"] !== undefined );
});

When("user sends a user creation request", function(context) {
var client = new XMLHttpRequest();
client.open('POST', 'https://reqres.in/api/users', false);
var data = {"email": "tester@froglogic.com"};
client.send(data);
context.userData["sentData"] = data;
context.userData["status"] = client.status;
context.userData["response"] = JSON.parse(client.response);
});

Then("the server returns a success status code", function(context) {
var status = context.userData["status"];
test.verify( status == 200 || status == 201 || status == 204 );
});

Then("a payload containing user data", function(context) {
var response = context.userData["response"];
test.verify( response["id"] !== undefined );
test.verify( response["createdAt"] !== undefined );
});

Given("the user has fetched the list of all colors", function(context) {
var client = new XMLHttpRequest();
var current_page = 1;
var last_page;
var colors = [];

do {
client.open('GET', 'https://reqres.in/api/colors?page='+current_page.toString(), false);
client.send();

var response = JSON.parse(client.response);
last_page = response["total_pages"];

for (var i =0; i < response["data"].length; ++i) {
colors.push(response["data"][i]);
}
current_page++;
} while (current_page != last_page) ;

context.userData["status"] = client.status;
context.userData["colors"] = colors;
context.userData["response"] = JSON.parse(client.response);
});

function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}

When("user sends a delete request for one of them", function(context) {
var random = getRandomInt( context.userData["colors"].length - 1 );

var client = new XMLHttpRequest();
client.open('DELETE', 'https://reqres.in/api/colors/'+random.toString(), false);
client.send();
context.userData["status"] = client.status;
});

Technical Approach

It is common in this approach to have technical information be displayed in the steps, clearly stating the input and output that is expected from the tested product.

Feature: Testing a REST API

Scenario: User registration is unsuccessful without password
When user sends 'POST' request to '/api/register' with the following data:
| email |
| tester@froglogic.com |
Then the server returns '400' as status code
And the following payload:
| error |
| Missing password |

Scenario Outline: User can create multiple users
When user sends 'POST' request to '/api/users' with the following data:
| email |
| <mail_address> |
Then the server returns '201' as status code
And the following payload:
| id | createdAt |
Examples:
| mail_address |
| xx@xx.x |

Scenario: User can browse the list of all available colors and delete one of them
Given the user has fetched all pages from '/api/colors'
And assign one of the id to 'random'
When user sends "DELETE" request to '/api/colors/<random>'
Then the server returns '204' as status code

import * as names from 'names.js';

When("user send '|word|' request to '|any|' with the following data:", function(context, method, url) {
var data = {};
var table = context.table;
var headers = table.shift();

for(var j=0; j < headers.length; ++j){
data[headers[j]] = table[0][j];
}

var client = new XMLHttpRequest();
client.open(method, 'https://reqres.in'+url, false);
client.setRequestHeader("Content-Type", "application/json");
client.send('{"email":"test@aa"}');
context.userData["sentData"] = data;
context.userData["status"] = client.status;
context.userData["response"] = JSON.parse(client.response);
});

Then("the server return '|integer|' as status code", function(context, code) {
test.verify( context.userData["status"] == code);
});

Then("the following payload:", function(context) {
var table = context.table;
var properties = table.shift();
var payload = context.userData["response"];

for(var j=0; j < properties.length; ++j){
test.verify( payload[properties[j]] !== undefined );
if(table.length > 0){
test.verify(payload[properties[j]] == table[0][j]);
}
}
});

Given("the user have fetch all pages from '|any|'", function(context, url) {
var client = new XMLHttpRequest();
var current_page = 1;
var last_page;
var data = [];

do {
client.open('GET', 'https://reqres.in'+url+'?page='+current_page.toString(), false);
client.send();

var response = JSON.parse(client.response);
last_page = response["total_pages"];

for (var i =0; i < response["data"].length; ++i) {
data.push(response["data"][i]);
}
current_page++;
} while (current_page != last_page) ;

context.userData["status"] = client.status;
context.userData["data"] = data;
context.userData["response"] = JSON.parse(client.response);
});

function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}

Given("assign one of the id to '|word|'", function(context, name) {
context.userData[name] = getRandomInt( context.userData["data"].length - 1 );
});

When("user send '|word|' request to '|any|'", function(context, method, url) {
var client = new XMLHttpRequest();
client.open(method, 'https://reqres.in'+url, false);
client.send();
context.userData["status"] = client.status;

if(client.responseText.length > 0){
context.userData["response"] = JSON.parse(client.response);
}
});

Comments

    The Qt Company acquired froglogic GmbH in order to bring the functionality of their market-leading automated testing suite of tools to our comprehensive quality assurance offering.