Focus on API testing
Before starting off, below listed are the reasons why API testing should be encouraged:
- Identifies bugs before it goes to UI
- Effective testing at a lower level over high-level broad-stack testing
- Reduces future efforts to fix defects
- Time-saving
Well, QA practices are becoming more automation-centric with evolving requirements, but identifying the appropriate approach is the primary and the most essential step. This implies choosing a framework or a tool to develop a test setup which should be:
- Scalable
- Modular
- Maintainable
- Able to provide maximum test coverage
- Extensible
- Able to generate test reports
- Easy to integrate with source control tool and CI pipeline
To attain the goal, why not develop your own asset rather than relying on the ready-made tools like Postman, JMeter, or any? Let’s have a look at why you should choose ‘writing your own code’ over depending on the API testing tools available in the market:
- Customizable
- Saves you from the trap of limitations of a ready-made tool
- Freedom to add configurations and libraries as required and not really depend on the specific supported plugins of the tool
- No limit on the usage and no question of cost
- Let’s take Postman for example. If we are going with Newman (CLI of Postman), there are several efforts that are likely to evolve with growing or changing requirements. Adding a new test requires editing in Postman, saving it in the collection, exporting it again and running the entire collection.json through Newman. Isn’t it tedious to repeat the same process every time?
We can overcome such annoyance and meet our purpose using a self-built Jest framework using SuperTest. Come on, let’s dive in!

Source: school.geekwall
Why Jest?
Jest is pretty impressive.
- High performance
- Easy and minimal setup
- Provides in-built assertion library and mocking support
- Several in-built testing features without any additional configuration
- Snapshot testing
- Brilliant test coverage
- Allows interactive watch mode ( jest –watch or jest –watchAll )
Hold on. Before moving forward, let’s quickly visit Jest configurations, Jest CLI commands, Jest Globals and Javascript async/await for better understanding of the coming content.
Ready, set, go!
Creating a node project jest-supertest in our local and doing npm init. Into the workspace, we will install Jest, jest-stare for generating custom test reports, jest-serial-runner to disable parallel execution (since our tests might be dependent) and save these as dependencies.
npm install jest jest-stare jest-serial-runner --save-devTags to the scripts block in our package.json.
"scripts": {
"test": "NODE_TLS_REJECT_UNAUTHORIZED=0 jest --reporters default jest-stare --coverage --detectOpenHandles --runInBand --testTimeout=60000",
"test:watch": "jest --verbose --watchAll"
}npm run test command will invoke the test parameter with the following:
- NODE_TLS_REJECT_UNAUTHORIZED=0: ignores the SSL certificate
- jest: runs the framework with the configurations defined under Jest block
- –reporters: default jest-stare
- –coverage: invokes test coverage
- –detectOpenHandles: for debugging
- –runInBand: serial execution of Jest tests
- –forceExit: to shut down cleanly
- –testTimeout = 60000 (custom timeout, default is 5000 milliseconds)
Jest configurations:
[Note: This is customizable as per requirements]
"jest": {
"verbose": true,
"testSequencer": "/home/abc/jest-supertest/testSequencer.js",
"coverageDirectory": "/home/abc/jest-supertest/coverage/my_reports/",
"coverageReporters": ["html","text"],
"coverageThreshold": {
"global": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
}
}testSequencer: to invoke testSequencer.js in the workspace to customize the order of running our test files
touch testSequencer.jsBelow code in testSequencer.js will run our test files in alphabetical order.
const Sequencer = require('@jest/test-sequencer').default;
class CustomSequencer extends Sequencer {
sort(tests) {
// Test structure information
// https://github.com/facebook/jest/blob/6b8b1404a1d9254e7d5d90a8934087a9c9899dab/packages/jest-runner/src/types.ts#L17-L21
const copyTests = Array.from(tests);
return copyTests.sort((testA, testB) => (testA.path > testB.path ? 1 : -1));
}
}
module.exports = CustomSequencer;- verbose: to display individual test results
- coverageDirectory: creates a custom directory for coverage reports
- coverageReporters: format of reports generated
- coverageThreshold: minimum and maximum threshold enforcements for coverage results
Testing endpoints with SuperTest
SuperTest is a node library, superagent driven, to extensively test Restful web services. It hits the HTTP server to send requests (GET, POST, PATCH, PUT, DELETE ) and fetch responses.
Install SuperTest and save it as a dependency.
npm install supertest --save-dev"devDependencies": {
"jest": "^25.5.4",
"jest-serial-runner": "^1.1.0",
"jest-stare": "^2.0.1",
"supertest": "^4.0.2"
}All the required dependencies are installed and our package.json looks like:
{
"name": "supertestjest",
"version": "1.0.0",
"description": "",
"main": "index.js",
"jest": {
"verbose": true,
"testSequencer": "/home/abc/jest-supertest/testSequencer.js",
"coverageDirectory": "/home/abc/jest-supertest/coverage/my_reports/",
"coverageReporters": ["html","text"],
"coverageThreshold": {
"global": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
}
},
"scripts": {
"test": "NODE_TLS_REJECT_UNAUTHORIZED=0 jest --reporters default jest-stare --coverage --detectOpenHandles --runInBand --testTimeout=60000",
"test:watch": "jest --verbose --watchAll"
},
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^25.5.4",
"jest-serial-runner": "^1.1.0",
"jest-stare": "^2.0.1",
"supertest": "^4.0.2"
}
}Now we are ready to create our Jest tests with some defined conventions:
- describe block – assembles multiple tests or its
- test block – (an alias usually used is ‘it’) holds single test
- expect() – performs assertions
It recognizes the test files in __test__/ folder
- with .test.js extension
- with .spec.js extension
Here is a reference app for API tests.
Let’s write commonTests.js which will be required by every test file. This hits the app through SuperTest, logs in (if required) and saves authorization token. The aliases are exported from here to be used in all the tests.
[Note: commonTests.js, be created or not, will vary as per the test requirements]
touch commonTests.jsvar supertest = require('supertest'); //require supertest
const request = supertest('https://reqres.in/'); //supertest hits the HTTP server (your app)
/*
This piece of code is for getting the authorization token after login to your app.
const token;
test("Login to the application", function(){
return request.post(``).then((response)=>{
token = response.body.token //to save the login token for further requests
})
});
*/
module.exports =
{
request
//, token -- export if token is generated
}Moving forward to writing our tests on POST, GET, PUT and DELETE requests for the basic understanding of the setup. For that, we are creating two test files to also see and understand if the sequencer works.
mkdir __test__/
touch __test__/postAndGet.test.js __test__/putAndDelete.test.jsAs mentioned above and sticking to Jest protocols, we have our tests written.
postAndGet.test.js test file:
- requires commonTests.js into ‘request’ alias
- POST requests to api/users endpoint, calls supertest.post()
- GET requests to api/users endpoint, calls supertest.get()
- uses file system to write globals and read those across all the tests
- validates response returned on hitting the HTTP endpoints
const request = require('../commonTests');
const fs = require('fs');
let userID;
//Create a new user
describe("POST request", () => {
try{
let userDetails;
beforeEach(function () {
console.log("Input user details!")
userDetails = {
"name": "morpheus",
"job": "leader"
}; //new user details to be created
});
afterEach(function () {
console.log("User is created with ID : ", userID)
});
it("Create user data", async done => {
return request.request.post(`api/users`) //post() of supertest
//.set('Authorization', `Token $ {request.token}`) //Authorization token
.send(userDetails) //Request header
.expect(201) //response to be 201
.then((res) => {
expect(res.body).toBeDefined(); //test if response body is defined
//expect(res.body.status).toBe("success")
userID = res.body.id;
let jsonContent = JSON.stringify({userId: res.body.id}); // create a json
fs.writeFile("data.json", jsonContent, 'utf8', function (err) //write user id into global json file to be used
{
if (err) {
return console.log(err);
}
console.log("POST response body : ", res.body)
done();
});
})
})
}
catch(err){
console.log("Exception : ", err)
}
});
//GET all users
describe("GET all user details", () => {
try{
beforeEach(function () {
console.log("GET all users details ")
});
afterEach(function () {
console.log("All users' details are retrieved")
});
test("GET user output", async done =>{
await request.request.get(`api/users`) //get() of supertest
//.set('Authorization', `Token ${request.token}`)
.expect(200).then((response) =>{
console.log("GET RESPONSE : ", response.body);
done();
})
})
}
catch(err){
console.log("Exception : ", err)
}
});putAndDelete.test.js file:
- requires commonsTests into ‘request’ alias
- calls data.json into ‘data’ alias which was created by the file system in our previous test to write global variables into it
- PUT sto api/users/${data.userId} endpoint, calls supertest.put()
- DELETE requests to api/users/${data.userId} endpoint, calls supertest.delete()
- validates response returned by the endpoints
- removes data.json (similar to unsetting global variables) after all the tests are done
const request = require('../commonTests');
const fs = require('fs'); //file system
const data = require('../data.json'); //data.json containing the global variables
//Update user data
describe("PUT user details", () => {
try{
let newDetails;
beforeEach(function () {
console.log("Input updated user's details");
newDetails = {
"name": "morpheus",
"job": "zion resident"
}; // details to be updated
});
afterEach(function () {
console.log("user details are updated");
});
test("Update user now", async done =>{
console.log("User to be updated : ", data.userId)
const response = await request.request.put(`api/users/${data.userId}`).send(newDetails) //call put() of supertest
//.set('Authorization', `Token ${request.token}`)
.expect(200)
expect(response.body.updatedAt).toBeDefined();
console.log("UPDATED RESPONSE : ", response.body);
done();
})
}
catch(err){
console.log("ERROR : ", err)
}
});
//DELETE the user
describe("DELETE user details", () =>{
try{
beforeAll(function (){
console.log("To delete user : ", data.userId)
});
test("Delete request", async done =>{
const response = await request.request.delete(`api/users/${data.userId}`) //invoke delete() of supertest
.expect(204)
console.log("DELETE RESPONSE : ", response.body);
done();
});
afterAll(function (){
console.log("user is deleted!!")
fs.unlinkSync('data.json'); //remove data.json after all tests are run
});
}
catch(err){
console.log("EXCEPTION : ", err);
}
});And we are done with setting up a decent framework and just a command away!
npm testOnce complete, the test results will be immediately visible on the terminal.



Test results HTML report is also generated as index.html under jest-stare/

And test coverage details are created under coverage/my_reports/ in the workspace.


Similarly, other HTTP methods can also be tested, like OPTIONS – supertest.options() which allows dealing with CORS, PATCH – supertest.patch(), HEAD – supertest.head() and many more.
Wasn’t it a convenient and successful journey?
Conclusion
So, wrapping it up with a note that API testing needs attention, and as a QA, let’s abide by the concept of a testing pyramid which is nothing but the mindset of a tester and how to combat issues at a lower level and avoid chaos at upper levels, i.e. UI.

I hope you had a good read. Kindly spread the word. Happy coding!