【问题标题】:How to write testable controllers with private methods in AngularJs?如何在 AngularJs 中使用私有方法编写可测试的控制器?
【发布时间】:2013-04-12 08:53:40
【问题描述】:

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

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

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 ninjas 提供一些帮助,一个工作小提琴会是完美的。

【问题讨论】:

    标签: testing angularjs tdd


    【解决方案1】:

    您提供的控制器函数将被 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 我相信这是您不想暴露在范围内的仅由单个控制器使用的小功能的方法。
    • ...直到 ControllerAs 出现。现在呢?
    • @ShawnErquhart controllerAs 有什么问题? this.util 非常适合模拟目的,不是吗?有什么问题?
    • @ShawnErquhart 与控制器作为,添加到此将添加到范围对吗?
    • @Ezeewei,向this 添加一个函数使其在视图中可用,这违背了将其设为私有的目的。
    【解决方案2】:

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

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

    【讨论】:

    • 澄清一下,我认为您拥有的任何封装逻辑都应该被注入,并且您的控制器应该负责将这些注入的逻辑参与者与范围变量组装起来用于传递到模板。
    • 听起来不错,但有时我们想要测试的东西很简单:if($scope.flag) service.a(); else service.b();。只为这个提取一些组件感觉不太自然。但是我明白你的意思,我认为毕竟这是一种有效的方法。
    • @ŁukaszBachman 如果你提取到私有方法中的代码太简单,无法提取到单独的mockable服务中,为什么不直接忽略Ctrl的内部代码分区,直接验证这个逻辑呢?即,在您的示例中,不是验证对util() 的调用,而是验证对anyService.doSmth() 的调用(以及可能在util() 中的任何其他服务调用)。
    • @Abbondanza 是正确的。如果由于逻辑问题应该将 util 分开,则应将其分解为单独的工厂。但不应仅仅为了简化测试而将其分离出来。请参阅我的答案以了解如何执行此操作。
    【解决方案3】:

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

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

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

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

    【讨论】:

    • downvoter - 请解释一下。如果声称我没有回答这个问题 - 这是不正确的。我解释说要测试私有方法,你应该测试所需的效果——而不是调用特定的方法。
    • 是的,即使不解释也讨厌反对票 :) 所有反对票都应该强制解释 :)
    • 我看不出测试是否调用了 anyService.doSmith() 与测试是否调用了 util() 有何不同。它们都是你的类的实现细节。唯一的区别是一个是外部依赖,一个不是。是的,创建依赖于实现细节的单元测试很脆弱,但这对你来说就是单元测试。
    • 区别在于anyService.doSmth() 是一个重要的函数,而util() 不是——它只是一个包装器/帮助器函数。我需要确保调用了 doSmth 函数,所以我对此进行了测试。这个例子并没有说明为什么它会有所作为——但它的想法是 doSmth 是实际上在做某事的函数——util 正在帮助我组织我的代码。如果我有多个具有不同动作和辅助函数的事件来组织哪些事件具有哪些动作 - 辅助函数是无关紧要的 - 只是实际发生的情况。
    【解决方案4】:

    我正在添加一个包含我当前方法的答案,希望得到一些 cmets,并可能引发关于这是否是一个好的解决方案的讨论。

    我们将私有函数附加到控制器函数(从而使它们公开,从而启用模拟)。为了避免必须一直重复控制器名称并使语法更具吸引力,我们正在创建 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();
    

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

    【讨论】:

    • (有偏见)但我认为在“私有命名​​空间”下添加嵌套层可以使意图更清晰 - 无论是在 $scope 上还是在控制器类本身上。
    • 是的,我同意。我只是不想将任何东西附加到$scope,因为我们已经实施了项目范围的规则,只有在 Angular 的摘要周期中需要评估的对象才能附加到它。我同意,您的解决方案更易于管理。
    猜你喜欢
    • 2015-07-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-28
    • 1970-01-01
    • 2011-05-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多