在Angular中的unit testing指令控制器,而不使控制器成为全局的
在Vojta Jina出色的存储库中,他演示了指令的testing,他定义了模块包装器之外的指令控制器。 看到这里: https : //github.com/vojtajina/ng-directive-testing/blob/master/js/tabs.js
这不是不好的做法,并污染全球命名空间?
如果有另一个地方可能是合理的调用TabsController,是不是会打破东西?
上述指令的testing可以在这里find: https : //github.com/vojtajina/ng-directive-testing/commit/test-controller
是否有可能独立于指令的其余部分testing指令控制器,而不将控制器置于全局名称空间中?
将整个指令封装在app.directive(…)定义中将会很好。
很好的问题!
所以,这是一个普遍的问题,不仅是控制器,还有可能需要一个指令来执行它的工作,但不一定要把这个控制器/服务暴露给“外部世界”。
我坚信全球数据是邪恶的,应该避免,这也适用于指令控制者 。 如果我们采取这个假设,我们可以采取几种不同的方法来“定位”这些控制器。 当我们这样做的时候,我们需要记住, 控制器应该仍然是“容易”被unit testing访问的,所以我们不能简单地把它隐藏到指令的closures中。 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 }; })
这是一个简单的技术,我们正在使用基于Vojta的工作https://github.com/angular-ui/bootstrap/blob/master/src/tabs/tabs.js 。
虽然这是一个非常简单的技术,但应该注意,控制器仍然暴露于整个应用程序,这意味着其他模块可能会覆盖它。 从这个意义上说,它使得AngularJS应用程序成为本地控制器(所以不会污染全局窗口范围),但对所有AngularJS模块也是全局的。
2) 使用闭合范围和特殊文件设置进行testing 。
如果我们想完全隐藏一个控制器函数,我们可以将代码封装在闭包中。 这是AngularJS正在使用的技术。 例如,查看NgModelController,我们可以看到它在自己的文件中被定义为一个“全局”函数(因此可以方便地进行testing),但整个文件在编译期间封装在闭包中:
- https://github.com/angular/angular.js/blob/master/src/angular.prefix
- https://github.com/angular/angular.js/blob/master/src/angular.suffix
总结一下:选项(2)是“更安全”的,但是需要一些预先设置的构build。
我喜欢有时包括我的控制器与指令,所以我需要一种方法来testing。
首先是指令
angular.module('myApp', []) .directive('myDirective', function() { return { restrict: 'EA', scope: {}, controller: function ($scope) { $scope.isInitialized = true }, template: '<div>{{isInitialized}}</div>' } })
然后testing:
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文档 。
请注意,实例化指令意味着控制器和所有链接函数已经运行,因此可能会影响您的testing。
詹姆斯的方法适用于我。 一个小的转折是,当你有一个外部模板时,你必须在$ rootScope。$ digest()之前调用$ httpBackend.flush()来让angular度执行你的控制器。
我想这应该不是一个问题,如果你使用https://github.com/karma-runner/karma-ng-html2js-preprocessor
这样做有什么问题吗? 似乎更好,因为你避免把你的控制器放在全局名称空间,并能够testing你想要什么(即控制器),而不会不必要地编译HTML。
示例指令定义:
.directive('tabs', function() { return { restrict: 'EA', transclude: true, scope: {}, controller: function($scope, $attrs) { this.someExposedMethod = function() {}; }, templateUrl: 'template/tabs/tabs.html', replace: true };
然后在你的Jasminetesting中,询问你使用“name + Directive”(例如“tabsDirective”)创build的指令:
var tabsDirective = $injector.get('tabsDirective')[0]; // instantiate and override locals with mocked test data var tabsDirectiveController = $injector.instantiate(tabsDirective.controller, { $scope: {...} $attrs: {...} });
现在你可以testing控制器的方法:
expect(typeof tabsDirectiveController.someExposedMethod).toBe('function');