Unit testing javascript code with mocha, sinon and expect.js

Why/When Unit Testing

Any Apigee API proxy implementation is made up of various components (policies) stacked one after the other in request and response pipelines. These policies can be out-of-the-box Apigee policies that are developed by Apigee Engineering Team and configured by us using XML. They can also be extension policies which allow us to execute custom code that we write in Java, JavaScript or Python.

Arguably the most important testing for an API layer is integration testing. These tests simulate an API client sending certain request combinations and assert response received from the API. For Apigee integration testing, the objective is to test client to Apigee + Apigee to Target + Target to backend systems integration. If target systems' data is too volatile, general practice is to mock target API responses in order to have predictable data fed back into Apigee implementation.

The objective is for the whole integration test to touch/execute as many Apigee policies and as many conditional statements it can. If we can achieve such an integration test, why do we need unit testing?

From my personal experience, the cases that require unit testing are:

  1. Operations that our integration testing cannot intercept and therefore cannot assert. An example for this case can be service callouts to external APIs, such as Loggly integration somewhere in between client and target. Another example might be testing IP blacklisting/whitelisting - need to simulate different IP addresses coming in to Apigee with many proxies in the middle.
  2. Very important code - e.g. security code, encryption code, signature generators/validators.
  3. Where coverage is extremely important, e.g. security code (again)

Regarding point 3 above - there is no tooling exists today that provides us testing coverage for Apigee policies. There was a project to achieve that but I am not sure it's current progress/status.

Other nice-to-have advantages of unit testing over integration testing for Apigee implementations are:

  1. Code can be tested locally without the need to deploy it to Apigee first.
  2. This enables us to create hooks (i.e. git) to enforce testing with coverage before we deploy or commit.
  3. Much faster to execute than integration testing (no network activity, etc).

When it comes to unit test Apigee implementation, the recommendation is to concentrate on the custom code that you write rather than trying to unit test out-of-the-box Apigee policies. Those policies are already unit tested by Apigee Engineering Team pre release.

Implementation

For this sample unit testing implementation, I have chosen to use mocha as the unit testing framework, sinon for mocking and expect.js for assertions. Istanbul can be used to produce coverage reports with its mocha integration.

The messages that are logged in Loggly are in the following structure:

If response is success:

{
	"organization": "org1",
	"environment": "env1",
	"responseCode": 200,
	"isError": false
}

If response is failure:

{
	"organization": "org1",
	"environment": "env1",
	"responseCode": 400,
	"isError": true,
	"errorMessage": "Helpful error message here"
}

The “system under test” will be the following javascript code that pushes log messages to loggly asynchronously.

try {
	var responseCode = parseInt(context.getVariable('response.status.code'));

	var log = {
		org: context.getVariable('organization.name'),
		env: context.getVariable('environment.name'),
		responseCode: responseCode,
		isError: (responseCode >= 400)
	};

	if (log.isError) {
		log.errorMessage = context.getVariable('flow.error.message');
	}

	var logglyRequest = new Request(
			'https://loggly.com/aaa', 
			'POST', 
			{'Content-Type': 'application/json'}, 
			JSON.stringify(log)
	);
	httpClient.send(logglyRequest);
} catch (e) {}

In order for this code to execute successfully outside of Apigee, we should be able to mock Apigee context, httpClient object and Request object. The majority of the unit testing code below sets up the mocking infrastructure around the actual test. These code can be moved outside of the actual test implementation for maintainability. I am keeping them in the same code for readability for now. Actual test starts with “describe” method which starts the test feature. The code is commented heavily to provide guidance:

/*jshint expr: true*/


var expect = require('expect.js');
var sinon = require('sinon');

// this is the javascript file that is under test
var jsFile = './LogToLoggly.js';

GLOBAL.context = {
	getVariable: function(s) {},
	setVariable: function(s) {}
};

GLOBAL.httpClient = {
	send: function(s) {}
};

GLOBAL.Request = function(s) {};

var contextGetVariableMethod, contextSetVariableMethod;
var httpClientSendMethod;
var requestConstructor;

// This method will execute before every it() method in the test
// we are stubbing all Apigee objects and the methods we need here
beforeEach(function () {
	contextGetVariableMethod = sinon.stub(context, 'getVariable');
	contextSetVariableMethod = sinon.stub(context, 'setVariable');
	requestConstructor = sinon.spy(GLOBAL, 'Request');
	httpClientSendMethod = sinon.stub(httpClient, 'send');
});

// restore all stubbed methods back to their original implementation
afterEach(function() {
	contextGetVariableMethod.restore();
	contextSetVariableMethod.restore();
	requestConstructor.restore();
	httpClientSendMethod.restore();
});

// this is the Loggly test feature here
describe('feature: logging to Loggly', function() {
	it('should log success responses correctly', function() {
		contextGetVariableMethod.withArgs('organization.name').returns('org1');
		contextGetVariableMethod.withArgs('environment.name').returns('env1');
		contextGetVariableMethod.withArgs('response.status.code').returns('200');


		var errorThrown = false;
		try { requireUncached(jsFile);} catch (e) { errorThrown = true; }

		expect(errorThrown).to.equal(false);

		expect(httpClientSendMethod.calledOnce).to.be.true;
		expect(requestConstructor.calledOnce).to.be.true;
		var requestConstructorArgs = requestConstructor.args[0];
		expect(requestConstructorArgs[0]).to.equal('https://loggly.com/aaa');		
		expect(requestConstructorArgs[1]).to.equal('POST');		
		expect(requestConstructorArgs[2]['Content-Type']).to.equal('application/json');		
		var logObject = JSON.parse(requestConstructorArgs[3]);
		expect(logObject.org).to.equal('org1');		
		expect(logObject.env).to.equal('env1');		
		expect(logObject.responseCode).to.equal(200);		
		expect(logObject.isError).to.be.false;
		expect(logObject).to.not.have.property('errorMessage');
	});


	it('should log failure responses correctly', function() {
		contextGetVariableMethod.withArgs('organization.name').returns('org1');
		contextGetVariableMethod.withArgs('environment.name').returns('env1');
		contextGetVariableMethod.withArgs('response.status.code').returns('400');
		contextGetVariableMethod.withArgs('flow.error.message').returns('this is a helpful error message');


		var errorThrown = false;
		try { requireUncached(jsFile);} catch (e) { errorThrown = true; }


		expect(errorThrown).to.equal(false);


		expect(httpClientSendMethod.calledOnce).to.be.true;
		expect(requestConstructor.calledOnce).to.be.true;
		var requestConstructorArgs = requestConstructor.args[0];
		expect(requestConstructorArgs[0]).to.equal('https://loggly.com/aaa');		
		expect(requestConstructorArgs[1]).to.equal('POST');		
		expect(requestConstructorArgs[2]['Content-Type']).to.equal('application/json');		
		var logObject = JSON.parse(requestConstructorArgs[3]);
		expect(logObject.org).to.equal('org1');		
		expect(logObject.env).to.equal('env1');		
		expect(logObject.responseCode).to.equal(400);		
		expect(logObject.isError).to.be.true;
		expect(logObject.errorMessage).to.equal('this is a helpful error message');		
	});
});

// node.js caches modules that is imported using 'require'
// this utility function prevents caching between it() functions - don't forget that variables are global in our javascript file
function requireUncached(module){
    delete require.cache[require.resolve(module)];
    return require(module);
}

If you save both of these files in one folder, you can run the test with:

$ mocha <test-file.js>

E.g.


$ mocha LogToLogglyTest.js

  feature: logging to Loggly
    ✓ should log success responses correctly
    ✓ should log failure responses correctly

  2 passing (48ms)
Comments
munimanjunathka
Participant I

Thanks for above example very information. How do I verify a variable set in the javascript using contextSetVariableMethod ?

dchiesa1
Staff

I guess you'll use contextGetVariableMethod to verify the variable.

munimanjunathka
Participant I

How do i stub global parameters like context.proxyResponse.content or context.proxyRequest.url ?


					
				
			
			
			
				
			
			
			
			
			
			
		
manish8_s
Participant I

How can we stub context.setVariable('param','somevalue') and verify the value of param; ?

maheshdhummi
Community Visitor

Most of the things are deprected (example GLOBAL etc...) where can I refer?

maheshdhummi
Community Visitor

How can we write test for customise javascript property values?

examples : properties.source ?

jcconnell
Explorer

For other's who may land on this topic looking for unit testing information, there is an example implementation of the techniques suggested in this thread at the following link:

https://github.com/apigee/devrel/tree/main/references/cicd-pipeline/test/unit

Version history
Last update:
‎05-12-2015 06:52 AM
Updated by: