0

I am introducing javascript unit tests to my company's application. I've worked predominantly with AngularJS, but their framework of choice is Knockout. The code is relatively modular in design (thanks to Knockout), so I assumed it would be easy to add unit tests around the code (much like Angular is).

However, there is a further complication: the system uses require.js as an IoC container. Also, my test runner is Chutzpah, which is headless/in the Visual Studio Test Explorer. I have tried to mock out the dependencies using SquireJS, but I run into a snag when trying to instantiate the ViewModel: it wants certain dependencies that are set up in the Component and inherited. I try to load the component, which has further dependencies, and so on and so on. Below I have a dumbed-down version of the setup that outlines the point. What is the best way to instantiate this view model so that I can mock out the dependencies with Squire and run my tests?

Component

define(function (require) {

    var Boiler = require('Boiler'),
        BaseViewModel = require('../../../../core/baseClasses/BaseViewModel'),
        viewTemplate = require('text!./view.html'),
        viewModel = require('./viewModel'),
        nls = require('i18n!./nls/resources');

    var Component = function (initializingParameters) {
        //...blah blah blah setup things
    };

    return Component;
});

ViewModel

define(function (require) {
    var Constants = require('../../../../core/constants');
    var momDate = moment().format('MMM DD, YYYY');
    var Utils = require("../../../../model/utils");
    var utils = new Utils();
    var otherComponentUtils = require("../../../otherModule/otherComponent/OtherComponentUtils");
    var otherComponentUtils = new OtherComponentUtils();

    var ViewModel = function (globalContext, moduleContext, dataContext, domElement) {

        var Self = this;
        Self.super(moduleContext, dataContext, resPickerContext);
        var constants = new Constants();
        var utils = require("../../../../model/utils");
        var Utils = new utils();

        Self.Items = ko.observableArray().extend({ throttle: 500 });
        //a bunch more Knockout object setup

        Self.GetAuthorizationTypesArray = function (itemId) {
            dataContext.dataRequestHandler(dataContext.serviceConstants.GetTransactionTypes, { ItemId: itemId },
                function (data) {
                    if (data != null && data.length !== 0) {
                        Self.TransactionTypeArray(data);

                        var transactionTypeArray = Self.TransactionTypeArray();
                        if (transactionTypeArray.length) {
                            var paymentInfo = Self.PaymentInfo();
                            if (paymentInfo !== null && paymentInfo !== undefined && paymentInfo.IsSpecial) {
                                var childThing = Self.ChildThing();
                                dataContext.dataRequestHandler(dataContext.serviceConstants.GetChild, { ChildId: childThing.ChildId, SpecialId: childThing.SpecialID }, function (data) {

                                    var child = data[0];
                                    var specialTypeId = child.ListId;

                                    if (specialTypeId === 13)
                                        Self.BigThing.Type(1);
                                    else
                                        Self.BigThing.Type(2);

                                }, Self.ShowError);
                            }
                        }
                    }
                },
                Self.ShowError);
        }

    return ViewModel;
});

chutzpah.json

{
  "Framework": "jasmine",
  "TestHarnessReferenceMode": "AMD",
  "TestHarnessLocationMode": "SettingsFileAdjacent",
  "RootReferencePathMode": "SettingsFileDirectory",
  "References": [
    { "Path": "../../myWebApp/Scripts/libs/assets/plugins/jquery-1.10.2.min.js" },
    { "Path": "../../myWebApp/scripts/libs/moment/moment.min.js" },
    { "Path": "../../myWebApp/Scripts/libs/require/require.js" },
    { "Path": "unittest.main.js" }
  ],
  "Tests": [
    {
      "Path": "app",
      "Includes": [ "*.tests.js" ]
    }
  ]
}

unittest.main.js

"use strict";

require.config({
    paths: {
        'jasmine': ['jasmine/jasmine'],
        'jasmine-html': ['jasmine/jasmine-html'],
        'jasmine-boot': ['jasmine/boot'],
        squire: 'Squire'
    },
    shim: {
        'jasmine-html': {
            deps : ['jasmine']
        },
        'jasmine-boot': {
            deps : ['jasmine', 'jasmine-html']
        },
        'squire': {
            exports: 'squire'
        }
    },
    config: {
        text: {
            useXhr: function (url, protocol, hostname, port) { return true },
            //Valid values are 'node', 'xhr', or 'rhino'
            env: 'xhr'
        }
    },
    waitSeconds: 0
});

viewModel.tests.js

define(function (require) {
    var testTargetPath = '../../myWebApp/Scripts/app/modules/thisModule/myViewModel';

    describe('My View Model', function () {
        var mockDataContext;
        var mockResponseData;
        var injector;
        var viewModel;

        beforeEach(function () {
            var Squire = require('Squire');
            injector = new Squire();
        });

        beforeEach(function () {
            mockResponseData = {};

            mockDataContext = {
                dataRequestHandler: function (url, data, onSuccess) {
                    onSuccess(mockResponseData[url]);
                },
                serviceConstants: {
                    GetTransactionTypes: 'getTransactionTypes',
                    GetChild: 'getNewPolicy'
                }
            };

            injector.mock("dataContext", mockDataContext);
        });

        beforeEach(function (done) {
            injector.require([testTargetPath], function (ViewModel) {
                viewModel = new ViewModel();
                done();
            });
        });

        it('gets authorization type array', function () {
            spyOn(mockDataContext, 'dataRequestHandler').and.callThrough();
            mockResponseData = {
                'getTransactionTypes': [
                    { name: 'auth type 1', TransactionTypeId: 90210 },
                    { name: 'auth type 2', TransactionTypeId: 42 },
                ]
            };

            viewModel.GetAuthorizationTypesArray(9266);

            expect(mockDataContext.dataRequestHandler).toHaveBeenCalledWith('getTransactionTypes', { ItemId: 9266 });
            expect(viewModel.TransactionTypeArray()).toBe(mockResponseData);
        });
    });
});

To be specific, in the ViewModel, when the tests run, it complains that super is undefined.

cidthecoatrack
  • 1,441
  • 2
  • 18
  • 32

1 Answers1

0

Alright, turns out there were a number of issues I had to deal with:

  1. At first I thought the failing on super was because of an ES6 compatibility issue (since PhantomJS only supports ES5). However, it turns out that the project is not using ES6, but inheritance.js, which manually builds the super dependencies. That setup is done within the component section of Knockout in our solution, so I replicated it within the unit tests.
  2. I was not properly setting up Squire to inject my dependencies, and corrected that.

I did not make any changes to my component or view model, and my Chutzpah configuration stayed the same. However, unittest.main.js was updated, as were my tests:

unittest.main.js

"use strict";

require.config({
    paths: {
        'jasmine': ['jasmine/jasmine'],
        'jasmine-html': ['jasmine/jasmine-html'],
        'jasmine-boot': ['jasmine/boot'],
        squire: 'Squire',
        "baseViewModel": '../../myWebApp/Scripts/app/core/baseClasses/BaseViewModel'
    },
    shim: {
        'jasmine-html': {
            deps : ['jasmine']
        },
        'jasmine-boot': {
            deps : ['jasmine', 'jasmine-html']
        },
        'squire': {
            exports: 'squire'
        }
    },
    config: {
        text: {
            useXhr: function (url, protocol, hostname, port) { return true },
            //Valid values are 'node', 'xhr', or 'rhino'
            env: 'xhr'
        }
    },
    waitSeconds: 0
});

viewModel.tests.js

define(function (require) {
    var testTargetPath = '../../myWebApp/Scripts/app/modules/thisModule/myViewModel';

    describe('View Model', function () {
        var mockGlobalContext;
        var mockModuleContext;
        var mockDataContext;
        var mockResponseData;
        var injector;
        var viewModel;
        var mockUtils;

        beforeEach(function () {
            var Squire = require('Squire');
            injector = new Squire();
        });

        beforeEach(function () {
            mockResponseData = {};
            mockGlobalContext = {};
            mockUtils = {};
            mockModuleContext = {};

            mockDataContext = {
                dataRequestHandler: function (url, data, onSuccess) {
                    onSuccess(mockResponseData[url]);
                },
                serviceConstants: {
                    GetTransactionTypes: 'getTransactionTypes',
                    GetChild: 'getNewPolicy'
                }
            };

            injector.mock("../../../../model/utils", function () { return mockUtils; });

            spyOn(mockDataContext, 'dataRequestHandler').and.callThrough();
        });

        beforeEach(function (done) {
            injector.require([testTargetPath], function (ViewModel) {
                var BaseViewModel = require('baseViewModel');
                BaseObject.Create(ViewModel, BaseViewModel, [mockGlobalContext, mockModuleContext, mockDataContext]);

                viewModel = new ViewModel(mockGlobalContext, mockModuleContext, mockDataContext);
                done();
            });
        });

        it('gets authorization type array', function () {
            mockResponseData = {
                'getGatewayTransactionTypes': [
                    { name: 'auth type 1', TransactionTypeId: 90210 },
                    { name: 'auth type 2', TransactionTypeId: 42 },
                ]
            };

            viewModel.GetAuthorizationTypesArray(9266);

            expect(mockDataContext.dataRequestHandler).toHaveBeenCalledWith('getGatewayTransactionTypes', { ItemId: 9266 }, jasmine.any(Function), viewModel.ShowError);
            expect(viewModel.TransactionTypeArray()).toEqual(mockResponseData['getGatewayTransactionTypes']);
        });
    });
});

With these changes, the test runs successfully and passes.

cidthecoatrack
  • 1,441
  • 2
  • 18
  • 32