testing需要控制器的指令

所以我看到另一个问题: 如何在指令UT中模拟所需的指令控制器,这基本上是我的问题,但似乎这个线程的答案是“改变你的devise”。 我想确保没有办法做到这一点。 我有一个指令,声明一个由儿童指令使用的控制器。 我现在试图编写茉莉花testing的儿童指令,但我不能让他们在testing编译,因为他们依赖于控制器。 这是它的样子:

addressModule.directive('address', ['$http', function($http){ return { replace: false, restrict: 'A', scope: { config: '=' }, template: '<div id="addressContainer">' + '<div ng-if="!showAddressSelectionPage" basic-address config="config"/>' + '<div ng-if="showAddressSelectionPage" address-selector addresses="standardizedAddresses"/>' + '</div>', controller: function($scope) { this.showAddressInput = function(){ $scope.showAddressSelectionPage = false; }; this.showAddressSelection = function(){ $scope.getStandardizedAddresses(); }; this.finish = function(){ $scope.finishAddress(); }; }, link: function(scope, element, attrs) { ... } } }]) 

小孩指令:

 addressModule.directive('basicAddress360', ['translationService', function(translationService){ return { replace: true, restrict: 'A', scope: { config: '=' }, template: '...', require: "^address360", link: function(scope, element, attrs, addressController){ ... } } }]) 

茉莉花testing:

 it("should do something", inject(function($compile, $rootScope){ parentHtml = '<div address/>'; subDirectiveHtml = '<div basic-address>'; parentElement = $compile(parentHtml)(rootScope); parentScope = parentElement.scope(); directiveElement = $compile(subDirectiveHtml)(parentScope); directiveScope = directiveElement.scope(); $rootScope.$digest(); })); 

我有没有办法用茉莉花testing这个子指令,如果是的话,我错过了什么? 即使我可以在没有控制器function的情况下testing指令,我也会很高兴。

我可以想到两种方法:

1)使用这两个指令

假设我们有以下指令:

 app.directive('foo', function() { return { restrict: 'E', controller: function($scope) { this.add = function(x, y) { return x + y; } } }; }); app.directive('bar', function() { return { restrict: 'E', require: '^foo', link: function(scope, element, attrs, foo) { scope.callFoo = function(x, y) { scope.sum = foo.add(x, y); } } }; }); 

为了testingcallFoo方法,您可以简单地编译这两个指令,并让bar使用foo的实现:

 it('ensures callFoo does whatever it is supposed to', function() { // Arrange var element = $compile('<foo><bar></bar></foo>')($scope); var barScope = element.find('bar').scope(); // Act barScope.callFoo(1, 2); // Assert expect(barScope.sum).toBe(3); }); 

工作Plunker 。

2)模拟foo的控制器

这个不太直截了当,有点棘手。 你可以使用element.controller()获取元素的控制器,并用Jasmine模拟它:

 it('ensures callFoo does whatever it is supposed to', function() { // Arrange var element = $compile('<foo><bar></bar></foo>')($scope); var fooController = element.controller('foo'); var barScope = element.find('bar').scope(); spyOn(fooController, 'add').andReturn(3); // Act barScope.callFoo(1, 2); // Assert expect(barScope.sum).toBe(3); expect(fooController.add).toHaveBeenCalledWith(1, 2); }); 

工作Plunker 。

当一个指令立即在其link函数中使用另一个指令时,棘手的部分就出现了:

 app.directive('bar', function() { return { restrict: 'E', require: '^foo', link: function(scope, element, attrs, foo) { scope.sum = foo.add(parseInt(attrs.x), parseInt(attrs.y)); } }; }); 

在这种情况下,您需要分别编译每个指令,以便在第二个指令使用之前模拟第一个指令。

 it('ensures callFoo does whatever it is supposed to', function() { // Arrange var fooElement = $compile('<foo></foo>')($scope); var fooController = fooElement.controller('foo'); spyOn(fooController, 'add').andReturn(3); var barElement = angular.element('<bar x="1" y="2"></bar>') fooElement.append(barElement); // Act barElement = $compile(barElement)($scope); var barScope = barElement.scope(); // Assert expect(barScope.sum).toBe(3); expect(fooController.add).toHaveBeenCalledWith(1, 2); }); 

工作Plunker 。

第一种方法比第二种方法更容易,但是它依赖于第一个指令的实现,也就是说,你不是unit testing的东西。 另一方面,虽然嘲笑指令的控制器并不是那么容易,但它使您能够更好地控制testing并消除对第一个指令的依赖。 所以,明智地select。 🙂

最后,我不知道有一个更简单的方法来做到上述所有。 如果有人知道更好的方法,请提高我的答案。

在迈克尔·本福德( 梦幻般的 )答案上叉。

如果你想在你的testing中完全隔离你的控制器/指令,你需要一个稍微不同的方法。

3)完全嘲弄任何需要的父控制器

将控制器与指令相关联时,控制器的实例将存储在元素的数据存储中。 键值的命名约定是'$'+指令的名称+'Controller' 。 每当Angular尝试parsing所需的控制器时,都会使用此约定遍历数据层次结构以find所需的控制器。 这可以通过将模拟控制器实例插入父元素来轻松操纵:

 it('ensures callFoo does whatever it is supposed to', function() { // Arrange var fooCtrl = { add: function() { return 123; } }; spyOn(fooCtrl, 'add').andCallThrough(); var element = angular.element('<div><bar></bar></div>'); element.data('$fooController', fooCtrl); $compile(element)($scope); var barScope = element.find('bar').scope(); // Act barScope.callFoo(1, 2); // Assert expect(barScope.sum).toBe(123); expect(fooCtrl.add).toHaveBeenCalled(); }); 

工作Plunker。

4)分离链接方法

在我看来,最好的办法是隔离链接方法。 所有以前的方法实际上testing的太多,当情况比这里提供的简单例子复杂一点时,他们需要太多的设置。

Angular对这种关注的分离提供了完美的支持:

 // Register link function app.factory('barLinkFn', function() { return function(scope, element, attrs, foo) { scope.callFoo = function(x, y) { scope.sum = foo.add(x, y); }; }; }); // Register directive app.directive('bar', function(barLinkFn) { return { restrict: 'E', require: '^foo', link: barLinkFn }; }); 

并通过改变我们以前包括我们的链接function…:

 inject(function(_barLinkFn_) { barLinkFn = _barLinkFn_; }); 

… 我们可以做的:

 it('ensures callFoo does whatever it is supposed to', function() { // Arrange var fooCtrl = { add: function() { return 321; } }; spyOn(fooCtrl, 'add').andCallThrough(); barLinkFn($scope, $element, $attrs, fooCtrl); // Act $scope.callFoo(1, 2); // Assert expect($scope.sum).toBe(321); expect(fooCtrl.add).toHaveBeenCalled(); }); 

工作Plunker。

这样我们只testing所关心的事情,如果需要,可以使用相同的方法来隔离编译函数。

5)注入指令定义并模拟控制器的function

另一种方法是注入指令的定义,并模拟我们需要的任何东西。 最好的事情是,你可以完全编写你的孩子指令的unit testing,而不依赖于你的父母。

使用inject(),您可以注入任何指令定义,提供指令名称+“指令”,然后访问其方法并根据需要replace它们

 it('ensures callFoo does whatever it is supposed to', inject(function(fooDirective) { var fooDirectiveDefinition = fooDirective[0]; // Remove any behavior attached to original link function because unit // tests should isolate from other components fooDirectiveDefinition.link = angular.noop; // Create a spy for foo.add function var fooAddMock = jasmine.createSpy('add'); // And replace the original controller with the new one defining the spy fooDirectiveDefinition.controller = function() { this.add = fooAddMock; }; // Arrange var element = $compile('<foo><bar></bar></foo>')($scope); var barScope = element.find('bar').scope(); // Act barScope.callFoo(1, 2); // Verify that add mock was called with proper parameters expect(fooAddMock).toHaveBeenCalledWith(1, 2); })); 

这个想法是由AngularJS Google Group的 Daniel Tabuenca提出的

在这个Plunker Daniel中嘲笑了ngModel指令