2015-07-12 102 views
1

我正在编写一些单元测试,遵循这个blog post中的惯例,但我无法访问我的指令的控制器功能。我有一个为ES指令和控制器编写的指令。我正在使用controllerAs将我的控制器类绑定到我的指令类。该指令我想写于容貌单元测试,像这样:问题单元测试Karma/Jasmine的ES6角度指令ControllerAs

// 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名称,后者是实际的类名称 - 不确定哪个是我想要的)我仍然收到错误: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 
    }); 
}); 

我指的是许多教程和博客后甚感为难和真的可以使用一些见解!

回答

0

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

通过编译标记和创建整个指令进行测试,基本上将其转化为集成测试,并且管理起来更复杂一些。一般而言,我发现这使得更多的可维护和有用的测试 - 特别是如果目标是测试控制器。

这将是类似于我的例子在这里:http://www.syntaxsuccess.com/viewarticle/writing-jasmine-unit-tests-in-es6

+0

啊我明白了。是的,我之前已经阅读过您的文章,我认为这是我将采取的方法。我想我忘了指令控制器与指令中的关注问题分离逻辑等。感谢您的回应。 –

0

我实现的解决方案是基于TGH我原来的问题作出回应。我现在将控制器类存储在一个单独的文件中,这会在指令转换功能和指令控制器功能之间创建一个关注点分离。该指令控制器文件看起来是这样的:

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; 

测试文件相应文件导入类和嘲笑为在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(); 
    }); 
});