Been working with Sails.js and was having trouble coming up with Jasmine unit tests for a controller. If this is something obvious, please pardon my ignorance, as I've only been deep-diving into JavaScript development for the past 3-4 months.
In past frameworks (specifically ASP .Net MVC), we had libraries to mock out any dependencies a controller might have to, say, an external service (via dependency injection). I kind of wanted to achieve the same level of unit testability with Sails.js so that we achieve a proper "unit" test. Specifically, for my case, I have a controller action with a dependency on a service object -- I simply want to mock the response of that service.
However, I'm having a heck of a time getting this Jasmine unit test to run (using the jasmine-node plugin). My code is below for both the controller and its unit test. What I'm getting right now is:
Is there anything blatantly obvious that I've clearly missed in my unit test? Code below. Thanks for any input!
UserController.js
var Battlefield4Service = require('../services/battlefield4Service');
module.exports = {
/**
* /user/bf4stats
*/
bf4Stats: function (req, res) {
var userName = req.param('userName');
var platform = req.param('platform');
var service = new Battlefield4Service();
service.getPlayerInfo(userName, platform,
function (data) {
// Success callback
res.json(data);
});
}
};
UserController.spec.js
var Sails = require('sails');
var userController = require('./UserController');
var FPSStatsDTO = require('../dto/fpsStatsDTO');
describe('UserController', function() {
// create a variable to hold the instantiated sails server
var app, req, res, rawObject, json;
// Setup mocked dependencies
beforeEach(function() {
// Lift Sails and start the server
Sails.lift({
log: {
level: 'error'
}
}, function(err, sails) {
app = sails;
//done(err, sails);
});
// Mocked Battlefield4Service
Battlefield4Service = {
getPlayerInfo: function (userName, platform, success) {
var dto = new FPSStatsDTO();
dto.userName = userName;
dto.platform = platform;
success(dto);
}
};
// req and res objects, mock out the json call
req = {
param: function(paramName) {
switch (paramName) {
case 'userName':
return 'dummyUser';
case 'platform':
return 'dummyPlatform';
}
}
};
res = {
json: function(object) {
rawObject = object;
json = JSON.stringify(object);
return json;
}
};
// Deploy 007
spyOn(req, 'param');
spyOn(res, 'json');
spyOn(Battlefield4Service, 'getPlayerInfo');
});
afterEach(function(){
app.lower();
});
it('Should call the Battlefield 4 Service', function() {
// Call the controller
userController.bf4Stats(req, res);
// Assertions
expect(req.param).toHaveBeenCalled();
expect(res.json).toHaveBeenCalled();
expect(Battlefield4Service.getPlayerInfo).toHaveBeenCalledWith(req.param('userName'), req.param('platform'));
expect(rawObject.userName).toEqual(req.param('userName'));
expect(rawObject.platform).toEqual(req.param('platform'));
expect(json).toNotBe(null);
expect(json).toNotBe(undefined);
});
});
Controller logic can be tested using automated integration tests, separate and distinct from unit tests for individual components. -1: A unit test for a controller could be pointless.
A typical unit test contains 3 phases: First, it initializes a small piece of an application it wants to test (also known as the system under test, or SUT), then it applies some stimulus to the system under test (usually by calling a method on it), and finally, it observes the resulting behavior.
For TypeScript, unit tests are run against the generated JavaScript code. In most TypeScript scenarios, you can debug a unit test by setting a breakpoint in TypeScript code, right-clicking a test in Test Explorer, and choosing Debug.
lift() Lift a Sails app programmatically. This does exactly what you might be used to seeing by now when you run sails lift . It loads the app, runs its bootstrap, then starts listening for HTTP requests and WebSocket connections.
UPDATE
Thinking further about the application architecture, it wasn't so much that I needed to test the request/response of the Sails.js controller -- in the context of this application, the controllers are very dumb in that they just pass through JSON objects. So, what I really needed to test was that my service was translating the external API's object to my application's internal DTO that will be used as a JSON return. In other words, it's more important for me to test the actual translation versus ensuring the controller passes it through, which we can safely assume will always be the case.
That being said, I switched my unit testing suite over from Jasmine to Chad's suggested combination of Mocha, Chai, and Sinon. The async hooks just look much cleaner in Mocha, imo. One added library that I used was Nock, a library designed to mock HTTP requests so I can intercept my service class' call to the API and return a stubbed object.
So, to recap, I ditched unit testing the controller, since it's superfluous for my use case. The important functionality I needed to test was the translation of an external API's object to my internal application's equivalent DTO.
Unit test below for the actual service. Note that this particular test didn't have a need for Sinon for stubbing/mocking as Nock took care of that for me:
var Sails = require('sails');
var sinon = require('sinon'); // Mocking/stubbing/spying
var assert = require('chai').assert; // Assertions
var nock = require('nock'); // HTTP Request Mocking
var constants = require('../constants/externalSystemsConstants');
var Battlefield4Service = require('./battlefield4Service');
describe('External Services', function () {
// create a variable to hold the instantiated sails server
var app, battlefield4Service;
// Global before hook
before(function (done) {
// Lift Sails and start the server
Sails.lift({
log: {
level: 'error'
}
}, function (err, sails) {
app = sails;
done(err, sails);
});
});
// Global after hook
after(function (done) {
app.lower(done);
});
describe('Battlefield 4 Service', function () {
var userName, platform, kills, skill, deaths, killAssists, shotsHit, shotsFired;
before(function () {
// Mock data points
userName = 'dummyUser';
platform = 'ps3';
kills = 200;
skill = 300;
deaths = 220;
killAssists = 300;
shotsHit = 2346;
shotsFired = 7800;
var mockReturnJson = {
player: {
name: userName,
plat: platform
},
stats: {
kills: kills,
skill: skill,
deaths: deaths,
killAssists: killAssists,
shotsHit: shotsHit,
shotsFired: shotsFired
}
};
// Mock response from BF4 API
battlefield4Service = nock('http://' + constants.BF4_SERVICE_URI_HOST)
.get(constants.BF4_SERVICE_URI_PATH.replace('[platform]', platform).replace('[name]', userName))
.reply(200, mockReturnJson);
});
it('Should translate BF4 API data to FPSStatsDTO', function (done) {
var service = new Battlefield4Service();
service.getPlayerInfo(userName, platform, function (fpsStats) {
assert(fpsStats !== null);
assert(fpsStats !== undefined);
assert(fpsStats.kills === kills, 'kills');
assert(fpsStats.deaths === deaths, 'deaths');
assert(fpsStats.killAssists === killAssists, 'deaths')
assert(fpsStats.kdr === kills / deaths, 'kdr');
assert(fpsStats.shotsFired === shotsFired, 'shotsFired');
assert(fpsStats.shotsHit === shotsHit, 'shotsHit');
assert(fpsStats.shotsAccuracy === shotsHit / shotsFired, 'shotsAccuracy');
assert(fpsStats.userName === userName, 'userName');
assert(fpsStats.platform === platform, 'platform');
done();
});
});
});
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With