SailsJS: Как правильно unit test контроллеры?

Работал с Sails.js, и у него возникли проблемы с установкой модулей Jasmine для контроллера. Если это что-то очевидное, прошу простить мое невежество, поскольку я занимаюсь разработкой JavaScript уже 3-4 месяца.

В прошлых рамках (в частности, ASP.Net MVC) у нас были библиотеки для извлечения любых зависимостей, которые контроллер мог бы иметь, скажем, из внешней службы (через инъекцию зависимостей). Я хотел бы достичь того же уровня модульности с Sails.js, чтобы мы достигли правильного "единичного" теста. В частности, для моего случая у меня есть действие контроллера с зависимостью от объекта службы - я просто хочу высмеять ответ этой службы.

Тем не менее, мне нужно потратить время на запуск этого Jasmine unit test (используя плагин jasmine- node). Мой код ниже для контроллера и его unit test. То, что я получаю сейчас, это:

  • Объект приложение, похоже, не разрешает в afterEach()
  • Утверждения о шпионах и переменных уровня теста не выполняются.

Есть ли что-то явно очевидное, что я явно пропустил в своем unit test? Код ниже. Спасибо за любой вклад!

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);
    });
});

Ответ 1

UPDATE

Размышляя о архитектуре приложения, мне было не так много, что мне нужно было проверить запрос/ответ контроллера Sails.js - в контексте этого приложения контроллеры очень глупы, поскольку они просто проходят через объекты JSON. Итак, что мне действительно нужно было проверить, так это то, что моя служба переводила внешний объект API в мое внутреннее DTO приложения, которое будет использоваться как возвращение JSON. Другими словами, для меня важнее проверить фактический перевод и обеспечить его контроль, который мы можем смело предположить, всегда будет иметь место.

Сказав это, я переключил свой блок тестирования модулей от Jasmine до Chad, предложив комбинацию Mocha, Chai и Sinon. Асинхронные крючки выглядят намного чище в Mocha, imo. Одна добавленная библиотека, которую я использовал, была Nock, библиотека, предназначенная для издевательства HTTP-запросов, чтобы я мог перехватить вызов моего класса сервиса API и вернуть заштрихованный объект.

Итак, чтобы повторить, я удалил блок тестирования контроллера, так как он лишний для моего использования. Важной функциональностью, которую мне нужно было проверить, был перевод внешнего объекта API на мое внутреннее приложение, эквивалентное DTO.

Unit test ниже для фактического обслуживания. Обратите внимание, что в этом конкретном тесте не было необходимости в Sinon для stubbing/mocking, поскольку Nock позаботился об этом для меня:

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();
            });
        });
    });
});

Ответ 2

Я заметил, что ваш вызов done() закомментирован в обратном вызове подъема парусов, и что в целом у вас есть ваш beforeEach, который определяется как синхронный крючок. Вам необходимо определить beforeEach как асинхронный крючок и убедиться, что вся ваша логика установки находится внутри обратного вызова подъема парусов, например:

beforeEach(function (done) {
    // Lift Sails and start the server
    Sails.lift({
        log: {
            level: 'error'
        }
    }, function (err, sails) {
        app = sails;
        // put the rest of your setup code here
        done(err);
    });        
});

То же самое касается вашего afterEach:

afterEach(function (done){
    app.lower(done);
});

Как только это будет сделано, я бы предположил, что вы можете протестировать, как вы могли бы ожидать.

P.S. Если вы еще на ранней стадии тестирования sails.js, вам может потребоваться перейти на Mocha + Chai + Sinon. Mocha позволяет вам настроить один до() и после() крючок для набора тестов, который позволит вам только поднять паруса один раз (ускорение вашего тестирования очень сильно, если у вас много тестов). Chai + Sinon просто предоставляет библиотеку утверждений и насмешливую библиотеку, которую Жасмин предоставляет из коробки.