web-dev-qa-db-ja.com

AngularJsでプライベートメソッドを使用してテスト可能なコントローラーを作成する方法は?

申し分なく、私は長い間いくつかの問題につまずいてきました、そして私はコミュニティの他の人々から意見を聞きたいです。

最初に、いくつかの抽象的なコントローラーを見てみましょう。

_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忍者からの助けを期待します、作業フィドルは完璧でしょう。

56
ŁukaszBachman

スコープに名前空間を置くことは汚染です。やりたいことは、そのロジックを別の関数に抽出し、それをコントローラーに注入することです。つまり.

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"をモックで単体テストできます。

32
MikeMac

指定したコントローラー関数は、コンストラクターとして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/

42
yianis

別のアプローチでチャイムします。プライベートメソッドをテストするべきではありません。それがプライベートな理由です-使用法とは無関係な実装の詳細です。

たとえば、utilが複数の場所で使用されていたが、現在は他のコードリファクタリングに基づいて、この1つの場所でのみ呼び出されることに気付いた場合はどうなるでしょう。なぜ追加の関数呼び出しがあるのですか? anyService.doSmith()をインクルードするだけ$scope.whenClicked()上記の提案では、util()が呼び出されることをテストしていると仮定すると、テストを変更していなくてもテストは中断しますプログラムの機能。単体テストの主な価値の1つは、物事を壊さずにリファクタリングを簡素化することです。したがって、物事を壊さなかった場合、テストは失敗しないはずです。

必要なことは、_$scope.whenClicked_が呼び出されたときに、anyService.doSmth()も呼び出されるようにすることです。あなただけが必要です:

_spyOn(anyService,'doSmith')
scope.whenClicked();
expect(anyService.doSmith).toHaveBeenCalled();
_
7
Yehosef

私は現在のアプローチを含む回答を追加し、いくつかのコメントを得て、おそらくこれが良い解決策であるかどうかについて議論を刺激したいと思っています。

コントローラー関数にプライベート関数を追加しています(したがって、それらをパブリックにし、モックを有効にします)。コントローラー名を常に繰り返す必要がなく、構文をより魅力的にするために、コントローラー関数への参照を保持する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();

私はまだこれがあまり好きではありませんが、これはこれを行う最も簡単な方法だと思います。私は誰かがより良いアプローチを見つけることを望んでいます。

2
ŁukaszBachman