所以我确实看到了另一个问题:How to mock required directive controller in directive UT这基本上是我的问题,但似乎这个帖子的答案是“改变你的设计”。我想确保没有办法做到这一点。我有一个指令声明了一个由children指令使用的控制器。我现在正在尝试为children指令编写jasmine测试但是我不能让它们在测试中编译,因为它们依赖于控制器。这是它的样子:
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){
...
}
}
}])
茉莉花测试:
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();
}));
我没办法用茉莉花来测试子指令吗?如果有的话,我错过了什么?即使我可以在没有控制器功能的情况下测试指令本身,我也会很高兴。
答案 0 :(得分:74)
我可以想到两种方法:
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);
}
}
};
});
为了测试callFoo
方法,您可以简单地编译这两个指令,并让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);
});
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);
});
当一个指令在其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);
});
第一种方法比第二种方法容易,但它依赖于第一个指令的实现,即,你不是单元测试的东西。另一方面,虽然模拟指令的控制器并不那么容易,但它可以让您更好地控制测试并消除对第一个指令的依赖。所以,明智地选择。 :)
最后,我不知道更简单的方法来完成上述所有操作。如果有人知道更好的方法,请改进我的答案。
答案 1 :(得分:56)
分享Michael Benford的(梦幻般的)答案。
如果您想在测试中完全隔离控制器/指令,则需要稍微不同的方法。
3)完全模拟任何所需的父控制器
将控制器与指令关联时,控制器的实例将存储在元素的数据存储中。键值的命名约定是'$'+指令名+'控制器'。每当Angular尝试解析 required 控制器时,它都会使用此约定遍历数据层次结构以找到所需的控制器。 通过将模拟的控制器实例插入父元素中可以很容易地操作它:
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();
});
4)分离链接方法
在我看来,最好的方法是隔离链接方法。所有以前的方法实际测试得太多,当情况比这里提供的简单示例稍微复杂一点时,它们需要太多的设置。
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
};
});
通过更改我们的 beforeEach 以包含我们的链接功能......:
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();
});
这样我们只测试所关注的事情,如果需要,可以使用相同的方法来隔离编译函数。
答案 2 :(得分:9)
5)注入指令定义并模拟控制器的功能
另一种方法是注入指令的定义并模拟我们需要的任何东西。关于这一点最好的事情是,您可以完全为您的孩子指令编写单元测试,而不依赖于您的父母。
使用inject(),您可以注入任何指令定义,提供指令的名称+&#39;指令&#39;然后访问其方法并根据需要替换它们
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);
}));
这个想法是由Daniel Tabuenca
中的AngularJS Google Group提出的在这个Plunker Daniel模拟了ngModel指令