如何在AngularJs中使用私有方法编写可测试的控制器?

时间:2013-04-12 08:53:41

标签: testing angularjs tdd

好吧,所以我长期以来一直在绊倒一些问题,我想听听社区其他人的意见。

首先,让我们看看一些抽象的控制器。

function Ctrl($scope, anyService) {

   $scope.field = "field";
   $scope.whenClicked = function() {
      util();
   };

   function util() {
      anyService.doSmth();
   }

}

显然我们在这里:

  • 带有$scope并注入了一些服务的控制器的常规脚手架
  • 附加到范围的某些字段和功能
  • 私有方法util()

现在,我想在单元测试(Jasmine)中介绍这个类。但是,问题是我想验证当我点击(调用whenClicked())某个项目时将调用util()方法。我不知道该怎么做,因为在Jasmine测试中我总是得到错误,要么util()的模拟没有被定义或者没被调用。

注意:我并没有尝试修复这个特定的例子,我一般都在询问测试这样的代码模式。所以请不要告诉我“究竟是什么错误”。我问的是如何做到这一点,而不是如何解决这个问题。

我一直在尝试各种方法:

  • 显然我不能在我的单元测试中使用$scope因为我没有将此函数附加到此对象(它通常以消息Expected spy but got undefined或类似的结尾)
  • 我尝试通过Ctrl.util = util;将这些函数附加到控制器对象,然后验证Ctrl.util = jasmine.createSpy()之类的模拟,但在这种情况下Ctrl.util未被调用,因此测试失败
  • 我尝试将util()更改为附加到this对象并再次嘲笑Ctrl.util,但没有运气

好吧,我无法找到解决这个问题的方法,我期待JS忍者的一些帮助,一个工作小提琴将是完美的。

4 个答案:

答案 0 :(得分:42)

您提供的控制器功能将由Angular用作构造函数;在某些时候,将使用new调用它来创建实际的控制器实例。如果您确实需要在控制器对象中具有未暴露于$ scope但可用于间谍/存根/模拟的函数,则可以将它们附加到this

function Ctrl($scope, anyService) {

  $scope.field = "field";
  $scope.whenClicked = function() {
    util();
  };

  this.util = function() {
    anyService.doSmth();
  }
}

现在调用var ctrl = new Ctrl(...)或使用Angular $controller服务检索Ctrl实例时,返回的对象将包含util函数。

您可以在此处查看此方法:http://jsfiddle.net/yianisn/8P9Mv/

答案 1 :(得分:32)

将范围限制在范围内的是污染。你想要做的是将该逻辑提取到一个单独的函数中,然后将其注入到Controller中。即。

function Ctrl($scope, util) {

   $scope.field = "field";
   $scope.whenClicked = function() {
      util();
   };
}

angular.module("foo", [])
       .service("anyService", function(...){...})
       .factory("util", function(anyService) {
              return function() {
                     anyService.doSmth();
              };
       });

现在您可以使用Ctrl 以及“util”模拟单元测试。

答案 2 :(得分:7)

我打算采用不同的方法。你不应该测试私有方法。这就是为什么它们是私有的 - 这是一个与使用无关的实现细节。

例如,如果您意识到util已在多个地方使用但现在,基于其他代码重构,它只在这一个地方调用。为什么有一个额外的函数调用?只需在anyService.doSmith()中加入$scope.whenClicked()。根据上述建议,假设您正在测试util()被调用,即使您没有更改程序的功能,您的测试也会中断。单元测试的一个主要价值是简化重构而不会破坏事物,所以如果你没有破坏,测试不应该失败。

您需要做的是确保在调用$scope.whenClicked时,还会调用anyService.doSmth()。你只需要:

spyOn(anyService,'doSmith')
scope.whenClicked();
expect(anyService.doSmith).toHaveBeenCalled();

答案 3 :(得分:2)

我正在添加一个包含我当前方法的答案,希望得到一些评论,或许可以激发讨论这是否是一个很好的解决方案。

我们将私有函数附加到控制器函数(从而使它们公开,从而实现模拟)。为了避免必须一直重复控制器名称并使语法更具吸引力,我们创建了self对象,该对象保存对控制器函数的引用。所以它变成了:

function Ctrl($scope, anyService) {

   $scope.field = "field";
   $scope.whenClicked = function() {
      self.util();
   };

   var self = Ctrl; // For the sake of syntax simplicity only

   self.util = function() {
      anyService.doSmth();
   };

}

然后在单元测试中我们现在可以使用:

Ctrl.util = jasmine.createSpy("util()");
expect(Ctrl.util).toHaveBeenCalled();

我仍然不喜欢这个,但我认为这是最简单的方法。我希望有人会找到更好的方法。