64

在Vojta Jina的优秀存储库中,他演示了指令的测试,他定义了模块包装器之外的指令控制器。看到这里: https://github.com/vojtajina/ng-directive-testing/blob/master/js/tabs.jsAngular中的单元测试指令控制器,无需将控制器全局化

是不是不好的做法和污染全球命名空间?

如果有人在另一个地方调用TabsController可能是合乎逻辑的,那会不会破坏东西?

对于所提到的指令的测试是在这里找到:https://github.com/vojtajina/ng-directive-testing/commit/test-controller

是否有可能测试指令控制器从指示的其余部分分开,不将其控制在一个全局命名空间?

将整个指令封装在app.directive(...)定义中会很好。

+0

请问有什么要点测试指令的控制器与指令本身分开?我怀疑如果控制器的代码没有沿着指令的其余部分进行测试,那么你做错了什么。这有点像说我有这个类的方法,但我想单独测试它,所以我将把它放在全局范围内。我认为接受的答案(pkozlowski)涉及不好的练习,而詹姆斯的答案是正确的。 – Izhaki 2015-08-20 14:58:22

回答

57

优秀的问题!

所以,这是一个普遍的问题,不仅与控制器有关,而且可能与服务有关,指令可能需要执行其工作,但不一定要将此控制器/服务公开给“外部世界”。

我坚信全球数据是邪恶的,应该避免,这也适用于指令控制器以及。如果我们采取这种假设,我们可以采取几种不同的方法来“本地”定义这些控制器。在这样做的时候,我们需要记住,控制器应该仍然可以“容易”进入单元测试,所以我们不能简单地将它隐藏到指令的关闭中。 IMO可能性是:

1)首先,我们可以简单地定义在模块级指令的控制器,前::

angular.module('ui.bootstrap.tabs', []) 
    .controller('TabsController', ['$scope', '$element', function($scope, $element) { 
    ... 
    }]) 
.directive('tabs', function() { 
    return { 
    restrict: 'EA', 
    transclude: true, 
    scope: {}, 
    controller: 'TabsController', 
    templateUrl: 'template/tabs/tabs.html', 
    replace: true 
    }; 
}) 

这是一个简单的技术,我们正在使用https://github.com/angular-ui/bootstrap/blob/master/src/tabs/tabs.js这是基于Vojta的工作。

虽然这是一个非常简单的技术,但应该注意的是,控制器仍然暴露给整个应用程序,这意味着其他模块可能会覆盖它。从这个意义上说,它使得AngularJS应用程序成为本地控制器(所以不会污染全局窗口范围),但它对所有AngularJS模块也是全局的。

2)使用封闭范围和特殊文件设置进行测试

如果我们想完全隐藏控制器函数,我们可以将代码封装在闭包中。这是AngularJS正在使用的技术。例如,看着NgModelController我们可以看到,它是在其自己的文件定义为一个“全球性”的功能(并因此很容易测试访问),但在构建时整个文件被包裹在封闭:

综上所述:选项(2)为“安全”的,但需要为构建一个位的前期设置的。

+0

感谢您的回答!没想过! – 2013-03-09 19:44:31

+0

好问题和良好的anwser。对我来说,第一种方法是完美的。 – 2013-07-30 14:14:35

+1

关于方案2;我将如何访问testSpec中的闭包控制器定义? – Stevo 2013-10-17 07:20:57

70

我喜欢有时包括我的控制器与指令,所以我需要一种方法来测试。

首先指令

angular.module('myApp', []) 
    .directive('myDirective', function() { 
    return { 
     restrict: 'EA', 
     scope: {}, 
     controller: function ($scope) { 
     $scope.isInitialized = true 
     }, 
     template: '<div>{{isInitialized}}</div>' 
    } 
}) 

则测试:

describe("myDirective", function() { 
    var el, scope, controller; 

    beforeEach inject(function($compile, $rootScope) { 
    # Instantiate directive. 
    # gotacha: Controller and link functions will execute. 
    el = angular.element("<my-directive></my-directive>") 
    $compile(el)($rootScope.$new()) 
    $rootScope.$digest() 

    # Grab controller instance 
    controller = el.controller("myDirective") 

    # Grab scope. Depends on type of scope. 
    # See angular.element documentation. 
    scope = el.isolateScope() || el.scope() 
    }) 

    it("should do something to the scope", function() { 
    expect(scope.isInitialized).toBeDefined() 
    }) 
}) 

更多的方式来从一个实例化的指令得到的数据见angular.element documentation

请注意,实例化指令意味着控制器和所有链接函数已经运行,因此可能会影响您的测试。

+14

这是一个很好的答案,但是需要一个小的编辑:您需要传入指令名称以调用'el.controller()'。所以在上面的例子中,调用将是'el.controller(“myDirective”)'。 – fiznool 2014-07-31 09:19:26

+0

控制器没有名称。这只是指令定义的一部分。 'el.controller()'起作用。 – 2014-07-31 20:37:21

+4

@fiznool是正确的,至少根据官方[angular文档](https://docs.angularjs.org/api/ng/function/angular.element#methods)。请注意,这不是指令的控制器名称,而是指令名称本身! – ArtoAle 2014-09-16 13:38:37

9

詹姆斯的方法适用于我。一个小的转折是,当你有一个外部模板时,你必须在$ rootScope。$ digest()之前调用$ httpBackend.flush()以让角度执行你的控制器。

我想这不应该是一个问题,如果你正在使用https://github.com/karma-runner/karma-ng-html2js-preprocessor

+0

使用'$ httpBackend.flush()'是关键。拯救了我的一天! – rkrishnan 2016-11-29 18:35:28

4

有什么不妥做这种方式?似乎更好,因为你避免把你的控制器放在全局名字空间中,并且能够测试你想要的东西(即控制器)而不会不必要地编译html。

举例指令定义:

.directive('tabs', function() { 
    return { 
    restrict: 'EA', 
    transclude: true, 
    scope: {}, 
    controller: function($scope, $attrs) { 
     this.someExposedMethod = function() {}; 
    }, 
    templateUrl: 'template/tabs/tabs.html', 
    replace: true 
    }; 

然后在你的茉莉花测试,要求该指令您已使用 “姓名+指令”(如 “tabsDirective”。):

var tabsDirective = $injector.get('tabsDirective')[0]; 
// instantiate and override locals with mocked test data 
var tabsDirectiveController = $injector.instantiate(tabsDirective.controller, { 
    $scope: {...} 
    $attrs: {...} 
}); 

现在你可以测试控制器的方法:

expect(typeof tabsDirectiveController.someExposedMethod).toBe('function'); 
+1

@ pkozlowski.opensource我错过了这种方法? – jbmilgrom 2016-08-05 11:42:49

+0

我试过这种方法访问控制器,它工作正常。但是我被这种方式卡住了如何传递指令属性。我有一个像这样的指令 - >'' 我想访问ref-object -uri参数在单元测试中。 – Anita 2017-10-06 12:12:00

+0

我的答案只是测试控制器功能。您的示例“html”对应于引导下的指令,需要获得'$ compile'来测试属性参数的传递情况 – jbmilgrom 2017-10-06 12:28:49

0

使用IIFE,这是一种常见的技术,以避免全局命名空间冲突&它还可以节省棘手的内联体操,并在你的范围内提供自由。

(function(){ 

    angular.module('app').directive('myDirective', function(){ 
    return { 
     ............. 
     controller : MyDirectiveController, 
     ............. 
    } 
    }); 

    MyDirectiveController.$inject = ['$scope']; 

    function MyDirectiveController ($scope) { 

    } 

})();