SailsJS: How to properly unit test controllers?
sails.js, unit-testing
Solution
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();
});
});
});
});
Problem
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: - The app object doesn't seem to resolve in afterEach() - The assertions on the spies and the test-level variables are failing. 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); }); }); ```