57

好的,所以我很长时间以来一直在偶然发现一些问题,我想听听社区其他人的意见。

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

function Ctrl($scope, anyService) {

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

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

}

显然我们在这里:

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

现在,我想在单元测试(Jasmine)中介绍这个类。但是,问题是我想验证当我单击(调用whenClicked())某个项目时,该util()方法将被调用。我不知道该怎么做,因为在 Jasmine 测试中,我总是遇到错误,即 mock forutil()尚未定义或未调用。

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

我一直在尝试多种方法来解决这个问题:

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

好吧,我无法解决这个问题,我希望 JS ninjas 能提供一些帮助,一个工作小提琴将是完美的。

4

4 回答 4

41

您提供的控制器函数将被 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/

于 2013-11-26T20:49:56.567 回答
31

在范围上命名空间是污染。您要做的是将该逻辑提取到一个单独的函数中,然后将其注入到您的控制器中。IE

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”进行单元测试。

于 2013-05-17T18:59:06.110 回答
7

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

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

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

spyOn(anyService,'doSmith')
scope.whenClicked();
expect(anyService.doSmith).toHaveBeenCalled();
于 2015-05-13T08:41:30.620 回答
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();

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

于 2013-04-15T09:49:11.757 回答