问题单元测试 ES6 Angular 指令 ControllerAs with Karma/Jasmine

Problems Unit Testing ES6 Angular Directive ControllerAs with Karma/Jasmine

我正在按照此 blog post 中的约定编写一些单元测试,但我无法访问我的指令的控制器功能。我有一个指令和控制器用 ES6 classes 编写的指令。我正在使用 controllerAs 将我的控制器 class 绑定到我的指令 class。我尝试为其编写单元测试的指令如下所示:

// Why is this file included? | Refer to : http://www.michaelbromley.co.uk/blog/350/exploring-es6-classes-in-angularjs-1-x#_section-directives
import directiveFactory from '../../../directivefactory.js';

// ##Directive Definition
class sideNav {

    constructor() {

        this.template = 
        `
            <!-- SIDENAV -->
            <!-- hamburger menu toggle visible when the sidenav menu is toggled shut -->
            <span class="glyphicon glyphicon-menu-hamburger side-nav-hamburger dark-hamburger" set-class-when-at-top="fix-to-top" ng-click='vm.test(); vm.toggle();'></span>

            <!-- wraps all sidenav menu content -->
            <div ng-class='{ show: vm.open }' class="collapsible">

                <!-- hamburger menu toggle visible when the sidenav menu is toggled open -->
                <span class="glyphicon glyphicon-menu-hamburger side-nav-hamburger light-hamburger" ng-click='vm.test(); vm.toggle();'></span>

                <!-- brand-image -->
                <div class="side-nav-head" transclude-id="head"></div> <!-- component user content insertion point 1 -->

                <!-- navigation links -->
                <div class="side-nav-body" transclude-id="body"></div> <!-- component user content insertion point 2 -->

                <!-- footer -->
                <footer>

                </footer>

            </div><!-- end collapsible -->
            <!-- END SIDENAV -->
        `;
        this.restrict = 'E';
        this.scope = {};
        this.bindToController = {

        };
        this.transclude = true;
        this.controller = SideNavController;
        this.controllerAs = 'vm';
    }

    // ###Optional Link Function
    link (scope, elem, attrs, ctrl, transclude) {

        transclude ((clone) => {

            angular.forEach(clone, (cloneEl, value) => {

                // If the cloned element has attributes...
                if(cloneEl.attributes) {

                    // Get desired target ID...
                    var tId = cloneEl.attributes["transclude-to"].value;

                    // Then find target element with that ID...
                    var target = elem.find('[transclude-id="' + tId + '"]');

                    // Append the element to the target
                    target.append(cloneEl); 
                }
            });
        });
    }
}

// ###Directive Controller
class SideNavController {

    constructor($rootScope) {

        this.$rootScope = $rootScope;

        // Initiate the menu as closed
        this.open = false;

        // Upon instantiation setup necessary $rootScope listeners
        this.listen();
    }

    // ####listen()
    // *function*
    // Setup directive listeners on the $rootScope
    listen () {

        // Receives an event from the ng-click within the directive template
        // for the side-nav-item component
        this.$rootScope.$on('navigation-complete', (event) => {

            // Upon receiving event, toggle the menu to closed
            this.toggle();
        });
    }

    // ####toggle()
    // *function*
    // Toggle menu open or shut
    toggle() {

        this.open = !this.open;
    }

    // ####test()
    // *function*
    test() { // DEBUG

        console.log('tester'); // DEBUG
        console.log(this.visible); // DEBUG
        console.log(this.open); // DEBUG
    }
}

SideNavController.$inject = ['$rootScope'];

export default ['sideNav', directiveFactory(sideNav)];

我将此文件与另一个指令组件一起导入,以创建这样的模块:

import { default as sideNav } from './side-nav/side-nav.js';
import { default as sideNavItem } from './side-nav-item/side-nav-item.js';

let moduleName = 'sideNav';

let module = angular.module(moduleName, [])
    // #### Sidebar Nav Components
   .directive(...sideNav)
   .directive(...sideNavItem);

export default moduleName;

在我的单元测试中,我尝试在 beforeEach 中模拟控制器,但无论我将控制器名称用作 vm 还是 SideNavController(前者是 controllerAs 名称和后者是实际的 class 名称——不确定哪个是我想要的)我仍然收到错误:Error: [ng:areq] Argument 'vm/SideNavController' is not a function, got undefined

这是我的单元测试:

describe('Side Nav Directive', () => {

    let elem, scope, ctrl;

    // Mock our side-nav directive
    beforeEach(angular.mock.module('sideNav'));

    beforeEach(angular.mock.inject(($rootScope, $compile, $controller) => {

        // Define the directive markup to test with
        elem = angular.element(
            `
            <div>

                <!-- side-nav directive component -->
                <side-nav>

                    <!-- content insertion point 1 -->
                    <div transclude-to="head">

                        <img src alt="test_image">

                    </div>

                    <!-- content insertion point 2 -->
                    <div transclude-to="body">

                        <a href="#">Test Link</a>

                    </div>

                </side-nav>

            </div>
            `
        );

        scope = $rootScope.$new();

        $compile(elem)(scope);

        scope.$digest();

        ctrl = $controller('vm', scope);
    }));

    it("should toggle shut when angular view navigation completes", () => {

        expect(ctrl).toBeDefined(); // <----- this errors
    });
});

在参考了许多教程和博客文章后,我真的很困惑,真的可以使用一些见解!

我实际上会建议一种稍微不同的测试方法。如果目标是测试 SideNavController 中的逻辑,我会考虑将 class 移动到它自己的文件中。这样你就可以在指令和测试中导入它。以这种方式构建它可以让您更轻松地访问它,因为您可以在与指令本身完全隔离的情况下对其进行测试。

通过编译标记和创建整个指令来测试它基本上将其转变为集成测试并且管理起来有点复杂。 总的来说,我发现这有助于进行更易于维护和更有用的测试 - 特别是如果目标是测试控制器。

这与我这里的示例类似:http://www.syntaxsuccess.com/viewarticle/writing-jasmine-unit-tests-in-es6

我实施的解决方案是基于 TGH 对我的原始问题的回应。我现在将控制器 class 存储在一个单独的文件中,这在指令包含功能和指令控制器功能之间创建了关注点分离。指令控制器文件如下所示:

const ROOTSCOPE = new WeakMap();

// ###Directive Controller
class SideNavController {

    constructor($rootScope) {

        ROOTSCOPE.set(this, $rootScope);

        // Initiate the menu as closed
        this.open = false;

        // Upon instantiation setup necessary $rootScope listeners
        this.listen();
    }

    // ####listen()
    // *function*
    // Setup directive listeners on the $rootScope
    listen () {

        // Receives an event from the ng-click within the directive template
        // for the side-nav-item component
        ROOTSCOPE.get(this).$on('navigation-complete', (event) => {

            // Upon receiving event, toggle the menu to closed
            this.toggle();
        });
    }

    // ####toggle()
    // *function*
    // Toggle menu open or shut
    toggle() {

        this.open = !this.open;
    }
}

SideNavController.$inject = ['$rootScope'];

export default SideNavController;

测试文件从适当的文件中导入 class 并在 beforeEach 块中模拟控制器以进行测试。控制器的变量和函数都可用:

// Import the controller to be tested
import SideNavController from './SideNavController.js';

describe('SideNavController', () => {

    let $rootScope, vm;

    beforeEach(angular.mock.inject((_$rootScope_, _$controller_) => {

        $rootScope = _$rootScope_.$new();

        vm = _$controller_(SideNavController, {} );
    }));

    it('should be initialize the open variable to false', () => {

        expect(vm.open).toBeDefined();

        expect(vm.open).toBeFalsy();
    });

    it('should listen for a navigation event and call the toggle function when the event is caught', () => {

        // Create Jasmine spy to watch toggle function
        spyOn(vm, 'toggle');

        // Simulate navigation event propagation
        $rootScope.$emit('navigation-complete');

        expect(vm.toggle).toHaveBeenCalled();
    });
});