• United States+1
  • United Kingdom+44
  • Afghanistan (‫افغانستان‬‎)+93
  • Albania (Shqipëri)+355
  • Algeria (‫الجزائر‬‎)+213
  • American Samoa+1684
  • Andorra+376
  • Angola+244
  • Anguilla+1264
  • Antigua and Barbuda+1268
  • Argentina+54
  • Armenia (Հայաստան)+374
  • Aruba+297
  • Australia+61
  • Austria (Österreich)+43
  • Azerbaijan (Azərbaycan)+994
  • Bahamas+1242
  • Bahrain (‫البحرين‬‎)+973
  • Bangladesh (বাংলাদেশ)+880
  • Barbados+1246
  • Belarus (Беларусь)+375
  • Belgium (België)+32
  • Belize+501
  • Benin (Bénin)+229
  • Bermuda+1441
  • Bhutan (འབྲུག)+975
  • Bolivia+591
  • Bosnia and Herzegovina (Босна и Херцеговина)+387
  • Botswana+267
  • Brazil (Brasil)+55
  • British Indian Ocean Territory+246
  • British Virgin Islands+1284
  • Brunei+673
  • Bulgaria (България)+359
  • Burkina Faso+226
  • Burundi (Uburundi)+257
  • Cambodia (កម្ពុជា)+855
  • Cameroon (Cameroun)+237
  • Canada+1
  • Cape Verde (Kabu Verdi)+238
  • Caribbean Netherlands+599
  • Cayman Islands+1345
  • Central African Republic (République centrafricaine)+236
  • Chad (Tchad)+235
  • Chile+56
  • China (中国)+86
  • Christmas Island+61
  • Cocos (Keeling) Islands+61
  • Colombia+57
  • Comoros (‫جزر القمر‬‎)+269
  • Congo (DRC) (Jamhuri ya Kidemokrasia ya Kongo)+243
  • Congo (Republic) (Congo-Brazzaville)+242
  • Cook Islands+682
  • Costa Rica+506
  • Côte d’Ivoire+225
  • Croatia (Hrvatska)+385
  • Cuba+53
  • Curaçao+599
  • Cyprus (Κύπρος)+357
  • Czech Republic (Česká republika)+420
  • Denmark (Danmark)+45
  • Djibouti+253
  • Dominica+1767
  • Dominican Republic (República Dominicana)+1
  • Ecuador+593
  • Egypt (‫مصر‬‎)+20
  • El Salvador+503
  • Equatorial Guinea (Guinea Ecuatorial)+240
  • Eritrea+291
  • Estonia (Eesti)+372
  • Ethiopia+251
  • Falkland Islands (Islas Malvinas)+500
  • Faroe Islands (Føroyar)+298
  • Fiji+679
  • Finland (Suomi)+358
  • France+33
  • French Guiana (Guyane française)+594
  • French Polynesia (Polynésie française)+689
  • Gabon+241
  • Gambia+220
  • Georgia (საქართველო)+995
  • Germany (Deutschland)+49
  • Ghana (Gaana)+233
  • Gibraltar+350
  • Greece (Ελλάδα)+30
  • Greenland (Kalaallit Nunaat)+299
  • Grenada+1473
  • Guadeloupe+590
  • Guam+1671
  • Guatemala+502
  • Guernsey+44
  • Guinea (Guinée)+224
  • Guinea-Bissau (Guiné Bissau)+245
  • Guyana+592
  • Haiti+509
  • Honduras+504
  • Hong Kong (香港)+852
  • Hungary (Magyarország)+36
  • Iceland (Ísland)+354
  • India (भारत)+91
  • Indonesia+62
  • Iran (‫ایران‬‎)+98
  • Iraq (‫العراق‬‎)+964
  • Ireland+353
  • Isle of Man+44
  • Israel (‫ישראל‬‎)+972
  • Italy (Italia)+39
  • Jamaica+1876
  • Japan (日本)+81
  • Jersey+44
  • Jordan (‫الأردن‬‎)+962
  • Kazakhstan (Казахстан)+7
  • Kenya+254
  • Kiribati+686
  • Kosovo+383
  • Kuwait (‫الكويت‬‎)+965
  • Kyrgyzstan (Кыргызстан)+996
  • Laos (ລາວ)+856
  • Latvia (Latvija)+371
  • Lebanon (‫لبنان‬‎)+961
  • Lesotho+266
  • Liberia+231
  • Libya (‫ليبيا‬‎)+218
  • Liechtenstein+423
  • Lithuania (Lietuva)+370
  • Luxembourg+352
  • Macau (澳門)+853
  • Macedonia (FYROM) (Македонија)+389
  • Madagascar (Madagasikara)+261
  • Malawi+265
  • Malaysia+60
  • Maldives+960
  • Mali+223
  • Malta+356
  • Marshall Islands+692
  • Martinique+596
  • Mauritania (‫موريتانيا‬‎)+222
  • Mauritius (Moris)+230
  • Mayotte+262
  • Mexico (México)+52
  • Micronesia+691
  • Moldova (Republica Moldova)+373
  • Monaco+377
  • Mongolia (Монгол)+976
  • Montenegro (Crna Gora)+382
  • Montserrat+1664
  • Morocco (‫المغرب‬‎)+212
  • Mozambique (Moçambique)+258
  • Myanmar (Burma) (မြန်မာ)+95
  • Namibia (Namibië)+264
  • Nauru+674
  • Nepal (नेपाल)+977
  • Netherlands (Nederland)+31
  • New Caledonia (Nouvelle-Calédonie)+687
  • New Zealand+64
  • Nicaragua+505
  • Niger (Nijar)+227
  • Nigeria+234
  • Niue+683
  • Norfolk Island+672
  • North Korea (조선 민주주의 인민 공화국)+850
  • Northern Mariana Islands+1670
  • Norway (Norge)+47
  • Oman (‫عُمان‬‎)+968
  • Pakistan (‫پاکستان‬‎)+92
  • Palau+680
  • Palestine (‫فلسطين‬‎)+970
  • Panama (Panamá)+507
  • Papua New Guinea+675
  • Paraguay+595
  • Peru (Perú)+51
  • Philippines+63
  • Poland (Polska)+48
  • Portugal+351
  • Puerto Rico+1
  • Qatar (‫قطر‬‎)+974
  • Réunion (La Réunion)+262
  • Romania (România)+40
  • Russia (Россия)+7
  • Rwanda+250
  • Saint Barthélemy (Saint-Barthélemy)+590
  • Saint Helena+290
  • Saint Kitts and Nevis+1869
  • Saint Lucia+1758
  • Saint Martin (Saint-Martin (partie française))+590
  • Saint Pierre and Miquelon (Saint-Pierre-et-Miquelon)+508
  • Saint Vincent and the Grenadines+1784
  • Samoa+685
  • San Marino+378
  • São Tomé and Príncipe (São Tomé e Príncipe)+239
  • Saudi Arabia (‫المملكة العربية السعودية‬‎)+966
  • Senegal (Sénégal)+221
  • Serbia (Србија)+381
  • Seychelles+248
  • Sierra Leone+232
  • Singapore+65
  • Sint Maarten+1721
  • Slovakia (Slovensko)+421
  • Slovenia (Slovenija)+386
  • Solomon Islands+677
  • Somalia (Soomaaliya)+252
  • South Africa+27
  • South Korea (대한민국)+82
  • South Sudan (‫جنوب السودان‬‎)+211
  • Spain (España)+34
  • Sri Lanka (ශ්‍රී ලංකාව)+94
  • Sudan (‫السودان‬‎)+249
  • Suriname+597
  • Svalbard and Jan Mayen+47
  • Swaziland+268
  • Sweden (Sverige)+46
  • Switzerland (Schweiz)+41
  • Syria (‫سوريا‬‎)+963
  • Taiwan (台灣)+886
  • Tajikistan+992
  • Tanzania+255
  • Thailand (ไทย)+66
  • Timor-Leste+670
  • Togo+228
  • Tokelau+690
  • Tonga+676
  • Trinidad and Tobago+1868
  • Tunisia (‫تونس‬‎)+216
  • Turkey (Türkiye)+90
  • Turkmenistan+993
  • Turks and Caicos Islands+1649
  • Tuvalu+688
  • U.S. Virgin Islands+1340
  • Uganda+256
  • Ukraine (Україна)+380
  • United Arab Emirates (‫الإمارات العربية المتحدة‬‎)+971
  • United Kingdom+44
  • United States+1
  • Uruguay+598
  • Uzbekistan (Oʻzbekiston)+998
  • Vanuatu+678
  • Vatican City (Città del Vaticano)+39
  • Venezuela+58
  • Vietnam (Việt Nam)+84
  • Wallis and Futuna+681
  • Western Sahara (‫الصحراء الغربية‬‎)+212
  • Yemen (‫اليمن‬‎)+967
  • Zambia+260
  • Zimbabwe+263
  • Åland Islands+358
Thanks! We'll be in touch in the next 12 hours
Oops! Something went wrong while submitting the form.

Building A Scalable API Testing Framework With Jest And SuperTest

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:

  1. Customizable
  2. Saves you from the trap of limitations of a ready-made tool
  3. Freedom to add configurations and libraries as required and not really depend on the specific supported plugins of the tool
  4. No limit on the usage and no question of cost
  5. 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!

Jest and SuperTest

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-dev
view raw install_jest.js hosted with ❤ by GitHub

Tags 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"
}
view raw scripts hosted with ❤ by GitHub

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
}
}
}
view raw jest configs hosted with ❤ by GitHub

testSequencer: to invoke testSequencer.js in the workspace to customize the order of running our test files

touch testSequencer.js

Below 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"
}
view raw dependencies hosted with ❤ by GitHub

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"
}
}
view raw package.json hosted with ❤ by GitHub

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.js
view raw common_test.js hosted with ❤ by GitHub

var 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
}
view raw commonTests.js hosted with ❤ by GitHub

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.js
view raw test_file.js hosted with ❤ by GitHub

As 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 test
view raw test.js hosted with ❤ by GitHub

Once complete, the test results will be immediately visible on the terminal.

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

jest-stare report

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

Test coverage report
Test coverage details

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. 

Testing Pyramid

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

Get the latest engineering blogs delivered straight to your inbox.
No spam. Only expert insights.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Did you like the blog? If yes, we're sure you'll also like to work with the people who write them - our best-in-class engineering team.

We're looking for talented developers who are passionate about new emerging technologies. If that's you, get in touch with us.

Explore current openings

Building A Scalable API Testing Framework With Jest And SuperTest

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:

  1. Customizable
  2. Saves you from the trap of limitations of a ready-made tool
  3. Freedom to add configurations and libraries as required and not really depend on the specific supported plugins of the tool
  4. No limit on the usage and no question of cost
  5. 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!

Jest and SuperTest

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-dev
view raw install_jest.js hosted with ❤ by GitHub

Tags 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"
}
view raw scripts hosted with ❤ by GitHub

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
}
}
}
view raw jest configs hosted with ❤ by GitHub

testSequencer: to invoke testSequencer.js in the workspace to customize the order of running our test files

touch testSequencer.js

Below 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"
}
view raw dependencies hosted with ❤ by GitHub

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"
}
}
view raw package.json hosted with ❤ by GitHub

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.js
view raw common_test.js hosted with ❤ by GitHub

var 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
}
view raw commonTests.js hosted with ❤ by GitHub

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.js
view raw test_file.js hosted with ❤ by GitHub

As 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 test
view raw test.js hosted with ❤ by GitHub

Once complete, the test results will be immediately visible on the terminal.

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

jest-stare report

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

Test coverage report
Test coverage details

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. 

Testing Pyramid

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

Did you like the blog? If yes, we're sure you'll also like to work with the people who write them - our best-in-class engineering team.

We're looking for talented developers who are passionate about new emerging technologies. If that's you, get in touch with us.

Explore current openings