单元测试中的bindToController

时间:2014-09-14 20:35:57

标签: angularjs unit-testing

我在指令中使用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以测试问题

4 个答案:

答案 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);
  };
}));