我在指令中使用bindToController使隔离范围直接连接到控制器,如下所示:
app.directive('xx', function () {
return {
bindToController: true,
controller: 'xxCtrl',
scope: {
label: '@',
},
};
});
然后在控制器中我有一个默认情况,在HTML中没有指定标签:
app.controller('xxCtrl', function () {
var ctrl = this;
ctrl.label = ctrl.label || 'default value';
});
如何在Jasmine单元测试中实例化xxCtrl,以便测试ctrl.label?
describe('buttons.RemoveButtonCtrl', function () {
var ctrl;
beforeEach(inject(function ($controller) {
// What do I do here to set ctrl.label BEFORE the controller runs?
ctrl = $controller('xxCtrl');
}));
it('should have a label', function () {
expect(ctrl.label).toBe('foo');
});
});
检查this以测试问题
答案 0 :(得分:47)
在Angular 1.3中(见下文1.4 +)
深入研究AngularJS源代码我发现了一个名为$controller
的{{1}}服务的未记录的第三个参数(参见$controller source)。
如果为true,later
将返回一个带有属性$controller()
的函数,您可以在其中设置属性。
当您准备好实例化控制器时,调用该函数并使用构造函数中可用的属性实例化控制器。
你的例子会这样:
instance
这是一个更新的Plunker(必须升级Angular使它工作,现在是1.3.0-rc.4):http://plnkr.co/edit/tnLIyzZHKqPO6Tekd804?p=preview
请注意,可能不建议使用它,引用Angular源代码:
稍后实例化控制器:此机制用于创建 调用控制器构造函数之前的对象实例 本身。
这允许在之前将属性添加到控制器 构造函数被调用。主要用于隔离范围 $ compile中的绑定。
此功能不适用于应用程序,因此不适用 公开记录。
然而,缺少一个用describe('buttons.RemoveButtonCtrl', function () {
var ctrlFn, ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrlFn = $controller('xxCtrl', {
$scope: scope,
}, true);
}));
it('should have a label', function () {
ctrlFn.instance.label = 'foo'; // set the value
// create controller instance
ctrl = ctrlFn();
// test
expect(ctrl.label).toBe('foo');
});
});
来测试控制器的机制让我使用了它......也许Angular家伙应该考虑公开这个标志。
在引擎盖下它使用临时构造函数,我猜也可以自己编写。
您的解决方案的优点是构造函数不会被调用两次,如果属性没有像您的示例中那样具有默认值,则可能会导致问题。
Angular 1.4+(2015-12-06更新):
Angular团队在version 1.4.0中为此添加了直接支持。 (见#9425)
您只需将对象传递给bindToController: true
函数:
$controller
另见blog post。
答案 1 :(得分:3)
使用ES6对BindToController进行单元测试
如果使用ES6,您可以直接导入控制器并在不使用角度模拟的情况下进行测试。
<强>指令:强>
import xxCtrl from './xxCtrl';
class xxDirective {
constructor() {
this.bindToController = true;
this.controller = xxCtrl;
this.scope = {
label: '@'
}
}
}
app.directive('xx', new xxDirective());
<强>控制器:强>
class xxCtrl {
constructor() {
this.label = this.label || 'default value';
}
}
export default xxCtrl;
控制器测试:
import xxCtrl from '../xxCtrl';
describe('buttons.RemoveButtonCtrl', function () {
let ctrl;
beforeEach(() => {
xxCtrl.prototype.label = 'foo';
ctrl = new xxCtrl(stubScope);
});
it('should have a label', () => {
expect(ctrl.label).toBe('foo');
});
});
有关详情,请参阅此处: Proper unit testing of Angular JS applications with ES6 modules
答案 2 :(得分:1)
在我看来,这个控制器不是孤立地测试的,因为它永远不会孤立地工作:
app.controller('xxCtrl', function () {
var ctrl = this;
// where on earth ctrl.lable comes from???
ctrl.newLabel = ctrl.label || 'default value';
});
它与依赖于接收其范围属性的指令紧密耦合。它不可重复使用。从查看这个控制器,我不得不想知道这个变量来自哪里。它并不比内部使用外部范围的变量的泄漏函数更好:
function Leaky () {
... many lines of code here ...
// if we are here we are too tired to notice the leakyVariable:
importantData = process(leakyVariable);
... mode code here ...
return unpredictableResult;
}
现在我有一个漏洞函数,根据调用函数的任何范围内存在(或不存在)的变量leakyVariable
,它的行为是高度不可预测的。
不出所料,这个功能是测试的噩梦。这实际上是一件好事,也许是为了迫使开发人员将函数重写为更模块化和可重用的东西。这真的不难:
function Modular (outsideVariable) {
... many lines of code here ...
// no need to hit our heads against the wall to wonder where the variable comes from:
importantData = process(outsideVariable);
... mode code here ...
return predictableResult;
}
没有泄漏问题,并且非常容易测试和重复使用。对我来说,使用好的旧$scope
是一种更好的方式:
app.controller('xxCtrl', function ($scope) {
$scope.newLabel = $scope.label || 'default value';
});
简单,简单,易于测试。另外没有笨重的指令对象定义。
controllerAs
语法背后的原始推理是从父级继承的漏洞范围。但是,指令的隔离范围已经解决了这个问题。因此,我没有理由使用更大的漏洞语法。
答案 3 :(得分:0)
我找到了一种不是特别优雅的方式但至少起作用(如果有更好的选择留下评论)。
我们设置“来自”指令的值,然后我们再次调用控制器函数来测试它的作用。我已经帮助“invokeController”做了更多DRY。
例如:
describe('buttons.RemoveButtonCtrl', function () {
var ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('xxCtrl', {
$scope: scope,
});
}));
it('should have a label', function () {
ctrl.label = 'foo'; // set the value
// call the controller again with all the injected dependencies
invokeController(ctrl, {
$scope: scope,
});
// test whatever you want
expect(ctrl.label).toBe('foo');
});
});
beforeEach(inject(function ($injector) {
window.invokeController = function (ctrl, locals) {
locals = locals || {};
$injector.invoke(ctrl.constructor, ctrl, locals);
};
}));