Unit testing API Proxies developed in Apigee Edge using mocha.js

5 1 4,216

This article enables one to quickly get started with writing simple Mocha test cases. It covers how to install Mocha and get up and running with it quickly.

To get started with the Mocha installation, you need to have node.js and npm installed on your system.

To install Mocha, run this from the terminal:

npm install –g mocha

This installs Mocha globally so that it can be run from anywhere.

This is the folder structure that I have set up:

Mocha-Test

test

package.json

test

config.js

test.js

We will go over the contents of these files. My package.json looks as below:

{

"name": "mocha_scripts",

"version": "1.0.0",

"description": "",

"main": "index.js",

"directories": {

"test": "test"

},

"scripts": {

"test": "echo \"Error: no test specified\" && exit 1"

},

"author": "",

"license": "ISC",

"devDependencies": {

"chai": "^2.3.0",

"request": "^2.55.0",

"sinon": "^1.14.1",

"sinon-chai": "^2.7.0"

}

}

chai is a Behaviour Driven Development / Test Driven Development library for node and can be paired with any Javascript testing framework. Sinon is used to write standalone and test framework agnostic JavaScript test spies, stubs and mocks, while sinon-chai provides a set of custom assertions for using the sinon.js spy, stub and mocking framework with the chai assertion library and request is the standard npm module for making http and https calls.

From Mocha-Test/test, run “npm install”. This would pick the dependencies from package.json and install those dependencies. After running npm install, the directory structure would be as follows:

Mocha-Test

test

node_modules

chai

request

sinon

sinon-chai

package.json

test

config.js

test.js

Now that the dependencies are out of the way, we are ready to write the actual test cases. The test cases would go into the test.js file in the test sub-folder. The config.js file serves as a property or constants file that the test.js refers to. My config.js file looks like this:

module.exports = {

// service URLs

BASE_URL_OAUTH: ‘abc-test.apigee.net',

BASE_URL_LOCATION: ‘abc-test.apigee.net/v2/location',

PROTOCOL: 'https',

URL_WHITELIST: 'whitelist',

URL_TOKEN: 'v1/oauth/token',

URL_LOCATION: 'position',

URL_INVITATIONS: 'invitations',

URL_CALLBACK: 'callback',

CLIENT_ID: 'HaeVHIeWzjz6VlOFbciKb2iUX6U1rzyA',

CLIENT_SECRET: 'b3HPfUEUucPGxeom',

CLIENT_ID_BAD: 'HaeVHIeWzjz6VlOFbciKb2iUX6U1rzy',

SCOPE: 'LOCATION',

GRANT_TYPE_CC: 'client_credentials',

THROTTLE_DELAY: 1000

};

Assume that there are one or more API Proxies deployed in Apigee Edge with the following resources / conditional flows:

https://abc-test.apigee.net/v1/oauth/token

https://abc-test.apigee.net/v2/location/whitelist

https://abc-test.apigee.net/v2/location/position

https://abc-test.apigee.net/v2/location/invitations

https://abc-test.apigee.net/v2/location/callback

Also assume that there is a product in Edge containing the above proxies and that there is an app that contains this product with consumer key 'HaeVHIeWzjz6VlOFbciKb2iUX6U1rzyA' and consumer secret 'b3HPfUEUucPGxeom'.

Now we get to the actual test cases in test.js. The contents of test.js look like this:

var url = require('url');
var request = require('request');
var chai = require('chai');


var config = require('./config');


var expect = chai.expect;


describe('Location API Test Suite', function() {
	var __msisdn = …;
	var __validation_code = …;
	var __X_MLM_Callback_Key = …;
	var __appkey = …;
	var __access_token;

	
    this.timeout(10000);
    
    
    before(function(done) {
        var urlObj = {
            protocol: config.PROTOCOL,
            hostname: config.BASE_URL_OAUTH,
         	pathname: config.URL_TOKEN,
        };


        var options = {
        	url : url.format(urlObj),
        	qs : {
            	client_id : config.CLIENT_ID,
            	client_secret : config.CLIENT_SECRET,
            	grant_type: config.GRANT_TYPE_CC,
            	scope : config.SCOPE
        		},
        		followRedirect : false
       	 	};            


    	request.get(options, function (err, res, body) {
        expect(res.statusCode).to.equal(200);


        data = JSON.parse(res.body);


        __access_token = data['access_token'];
        expect(__access_token).to.not.be.empty;
        console.log("Access Token: " + __access_token);


        setTimeout(done, config.THROTTLE_DELAY);                
        });
    });
       	
   	describe('Location API positive test cases', function() {
   	
                
        it('should send invite to device', function(done) {
            var urlObj = {
                protocol: config.PROTOCOL,
                hostname: config.BASE_URL_LOCATION,
                pathname: config.URL_INVITATIONS,
            };
            var postData = {
  				msisdn: __msisdn				
			};


            var options = {
                url : url.format(urlObj), 
                body: postData,
                json: true,             
                headers : {
                    Authorization : 'Bearer ' + __access_token
                },
                followRedirect : false
            };


            request.post(options, function(err, res, body) {
                console.log("Invitations Response: " + JSON.stringify(res.body));
                expect(res.statusCode).to.equal(200);                
                //data = JSON.parse(res.body);                
                setTimeout(done, config.THROTTLE_DELAY);
            });
        }); 
        
        it('should resend invitation', function(done) {
            var urlObj = {
                protocol: config.PROTOCOL,
                hostname: config.BASE_URL_LOCATION,
                pathname: config.URL_INVITATIONS + '/' + __msisdn,
            };            


            var options = {
                url : url.format(urlObj),                           
                headers : {
                    Authorization : 'Bearer ' + __access_token
                },
                followRedirect : false
            };


            request.put(options, function(err, res, body) {
                console.log("Resend invitation Response: " + res.body);
                expect(res.statusCode).to.equal(200);                
                //data = JSON.parse(res.body);                
                setTimeout(done, config.THROTTLE_DELAY);
            });
        });
        
        it('should get location', function(done) {
            var urlObj = {
                protocol: config.PROTOCOL,
                hostname: config.BASE_URL_LOCATION,
                pathname: config.URL_LOCATION + '/' + __msisdn,
            };


            var options = {
                url : url.format(urlObj), 
                qs : {
                    type : 'live',
                    max_loc_age : '3600'                    
                },              
                headers : {
                    Authorization : 'Bearer ' + __access_token
                },
                followRedirect : false
            };


            request.get(options, function(err, res, body) {
                console.log("Location: " + res.body);
                expect(res.statusCode).to.equal(200);                
                //data = JSON.parse(res.body);                
                setTimeout(done, config.THROTTLE_DELAY);
            });
        });                 		                    
     });
     
    describe('Location API negative test cases', function() {
    
    var __msisdnExists = xxx;
    var __msisdnInvalid = xxx;    
        
        it('should return an invalid PIN error', function(done) {
            var urlObj = {
                protocol: config.PROTOCOL,
                hostname: config.BASE_URL_LOCATION,
                pathname: config.URL_INVITATIONS + '/' + __msisdn,
            };
            var postData = {
  				validation_code: __validation_code				
			};


            var options = {
                url : url.format(urlObj), 
                body: postData,
                json: true,             
                headers : {
                    Authorization : 'Bearer ' + __access_token
                },
                followRedirect : false
            };


            request.post(options, function(err, res, body) {
                console.log("Register device with PIN Invalid PIN Response: " + JSON.stringify(res.body));
                expect(res.statusCode).to.equal(502);  
                expect(res.body.code).to.equal('5020'); 
                expect(res.body.message).to.equal('Invalid PIN');              
                //data = JSON.parse(res.body);                
                setTimeout(done, config.THROTTLE_DELAY);
            });
        });
        
        it('should return an invalid MSISDN error (remove from whitelist)', function(done) {
            var urlObj = {
                protocol: config.PROTOCOL,
                hostname: config.BASE_URL_LOCATION,
                pathname: config.URL_WHITELIST + '/' + __msisdnInvalid,
            };            


            var options = {
                url : url.format(urlObj),                           
                headers : {
                    Authorization : 'Bearer ' + __access_token
                },
                followRedirect : false
            };


            request.del(options, function(err, res, body) {
                console.log("Remove from whitelist Response (MSISDN invalid): " + res.body);
                expect(res.statusCode).to.equal(400);                
                data = JSON.parse(res.body);  
                expect(data.code).to.equal(4006);               
                setTimeout(done, config.THROTTLE_DELAY);
            });
        });           
    });
});


url, request, chai and config are variables of the dependency modules, the expect variable is going to be used for making the assertions. (Note that we can also use the npm assert module for making assertions).

The describe function denotes that start of a test suite and can have nested describe functions within it. In this case, positive test cases and negative test cases are contained in separate nested describe functions within a main describe function that represents the overall test suite.

The before function executes before all the test cases within the describe function. In this example, the before function is in the main describe function and hence executes before the 2 nested describe functions (positive and negative test cases). In this function, a HTTP GET request is made to the URL https://abc-test.apigee.net/v1/oauth/token with the query string parameters client_id, client_secret, grant_type and scope. The values of these parameters are taken from the config.js file. The expect assertion is used to check if the response status code is equal to 200. The JSON response in the body is parsed, asserted to not be empty using expect and its value is assigned to a global variable that is used in the nested describe functions. This function effectively implements the OAuth Client Credentials 2-legged grant type to make a call to the OAuth Token endpoint to get the OAuth access token. This access token will be used in all the test cases as the end-points of these test cases expect the access token for authorization.

Every test case is described using an “it” function, for example it('should send invite to device', function(done) {…}. The positive test cases described in this example do the following:

  1. The “should send invite to device” function does a HTTP POST to https://abc-test.apigee.net/v2/location/invitations using the JSON payload {“msisdn”:xxxx} with the access token obtained from the before function in the Authorization header as a bearer token (Authorization : Bearer xxxxxxx). It then checks that the HTTP response status code is 200 using the expect chai assertion
  2. The “should resend invitation” function does a HTTP PUT to https://abc-test.apigee.net/v2/location/invitations/{msisdn} with no payload and with the bearer access token in the Authorization header and checks that the HTTP response status code is 200
  3. The “should get location” function does a HTTP GET to https://abc-test.apigee.net/v2/location/position with the query string type=live&max_loc_age=3600 and with the bearer access token in the Authorization header and checks that the HTTP response status code is 200

The negative test cases described in this example do the following:

  1. The “should return an invalid PIN error” function does a HTTP POST to https://abc-test.apigee.net/v2/location/invitations/{msisdn} with the JSON payload {“validation_code”:xxx} and with the bearer access token in the Authorization header. It then extracts the JSON response from the body and checks that the code in the JSON response is ‘5020’ and the message in the JSON response is ‘Invalid PIN’ in addition to checking that the HTTP status code of the response is 400
  2. The “should return an invalid MSISDN error (remove from whitelist)” function does a HTTP DELETE to https://abc-test.apigee.net/v2/location/whitelist/{msisdn} with the bearer token in the Authorization header, extracts the JSON response from the body and checks that the code in the JSON response is 4006 in addition to checking that the HTTP status code of the response is 400

The test cases are executed by typing the command mocha in the terminal from Mocha-Test/test. The output is in italics.

Location API Test Suite

Access Token: 7tBrAnQ5rsccUM0GtxLkg9L3dKlL

Location API positive test cases

Invitations Response: {"status":"400","code":"4002","message":"The user is already in whitelist"}

1)should send invite to device

Resend invitation Response:

{

"status": "400",

"code": "40010",

"message": "Invitation does not exist"

}

2)should resend invitation

Location:

{

"status": "400",

"code": "4009",

"message": "The requested quality of position cannot be provided"

}

3)should get location

Location API negative test cases

Register device with PIN Invalid PIN Response: {"status":"502","code":"5020","message":"Invalid PIN"}

should return an invalid PIN error (6225ms)

Remove from whitelist Response (MSISDN invalid): { "status": 400,"code":

4006,"message": "MSISDN provided is not valid" }

should return an invalid MSISDN error (remove from whitelist) (1913ms)

2 passing (59s)

3 failing

1) Location API Test Suite Location API positive test cases should send invite to device:

Uncaught AssertionError: expected 400 to equal 200

+ expected- actual

-400

+200

at Request._callback (test/test.js:128:43)

at Request.self.callback (node_modules/request/request.js:199:22)

at Request.<anonymous> (node_modules/request/request.js:1036:10)

at IncomingMessage.<anonymous> (node_modules/request/request.js:963:12)

at endReadableNT (_stream_readable.js:905:12)

2) Location API Test Suite Location API positive test cases should resend invitation:

Uncaught AssertionError: expected 400 to equal 200

+ expected- actual

-400

+200

at Request._callback (test/test.js:181:43)

at Request.self.callback (node_modules/request/request.js:199:22)

at Request.<anonymous> (node_modules/request/request.js:1036:10)

at IncomingMessage.<anonymous> (node_modules/request/request.js:963:12)

at endReadableNT (_stream_readable.js:905:12)

3) Location API Test Suite Location API positive test cases should get location:

Uncaught AssertionError: expected 400 to equal 200

+ expected- actual

-400

+200

at Request._callback (test/test.js:232:43)

at Request.self.callback (node_modules/request/request.js:199:22)

at Request.<anonymous> (node_modules/request/request.js:1036:10)

at IncomingMessage.<anonymous> (node_modules/request/request.js:963:12)

at endReadableNT (_stream_readable.js:905:12)

In this example, the positive test cases are all failing and the negative test cases are passing. Where there is a failure, we can see the expected and actual assertions and the stack trace of the failure. Where there is a successful execution of a test case, we can see a against the “it” clause.

This article covers only the basics and is intended to get started with Mocha. Further reading is recommended at https://mochajs.org for more advanced scenarios.

Comments

Thanks for sharing @Sathish Balasubramaniyan

Version history
Last update:
‎03-07-2016 04:56 PM
Updated by: