Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SailsJS: How to properly unit test controllers?

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:

  1. The app object doesn't seem to resolve in afterEach()
  2. 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);
    });
});
like image 847
grales Avatar asked Feb 20 '14 20:02

grales


People also ask

Should you write unit tests for controllers?

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.

How do you conduct unit testing?

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.

How do I run a TypeScript unit test?

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.

What sails lift?

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.


1 Answers

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();
            });
        });
    });
});
like image 139
grales Avatar answered Oct 19 '22 11:10

grales