在Jasmine单元测试中解耦Knockout依赖项

时间:2016-08-19 21:26:51

标签: unit-testing knockout.js requirejs jasmine chutzpah

我正在向我公司的应用程序引入javascript单元测试。我主要使用AngularJS,但他们选择的框架是Knockout。代码在设计上是相对模块化的(感谢Knockout),所以我假设在代码周围添加单元测试很容易(就像Angular一样)。

然而,还有一个复杂因素:系统使用require.js作为IoC容器。此外,我的测试运行器是Chutzpah,它在Visual Studio Test Explorer中是无头的。我试图使用SquireJS模拟出依赖关系,但是在尝试实例化ViewModel时我遇到了麻烦:它想要在Component中设置并继承的某些依赖项。我尝试加载具有进一步依赖性的组件,依此类推。下面我有一个简明扼要的设置版本,概述了这一点。实例化此视图模型的最佳方法是什么,以便我可以使用Squire模拟依赖项并运行我的测试?

组件

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

视图模型

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

具体而言,在ViewModel中,当测试运行时,它会抱怨super未定义。

1 个答案:

答案 0 :(得分:0)

好吧,事实证明我必须处理许多问题:

  1. 起初我认为super失败是因为ES6 兼容性问题(因为PhantomJS仅支持ES5)。然而, 事实证明该项目没有使用ES6,但是 inheritance.js,它手动构建超级依赖项。 该设置是在我们的Knockout的component部分内完成的 解决方案,所以我在单元测试中复制了它。
  2. 我没有正确设置Squire注入我的依赖项,并且 纠正了。
  3. 我没有对我的组件或视图模型进行任何更改,我的Chutzpah配置保持不变。但是,unittest.main.js已更新,我的测试也是如此:

    <强> 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']);
            });
        });
    });
    

    通过这些更改,测试成功运行并通过。