2013-04-12 21 views
56

申し訳ありませんので、私は長い間、問題を抱えており、残りのコミュニティからの意見を聞きたいと思います。AngularJsでプライベートメソッドを使ってテスト可能なコントローラを書く方法は?

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

function Ctrl($scope, anyService) { 

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

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

} 

明らかに私たちはここにある:

  • プライベートメソッドを$scopeとコントローラのための定期的な足場

    • をしてスコープに取り付けたいくつかのサービスが
    • を注入し、いくつかのフィールドと機能util()

    今、このクラスをユニットテストでカバーしたいと思います(Jasmi ne)。しかし、問題は、私がutil()メソッドが呼び出されるいくつかのアイテムを(コールwhenClicked())クリックすると確認したいということです。ジャスミンテストでは、util()のモックが定義されていないか、呼び出されていないというエラーが常に出ているので、私はその方法を知らない。

    注:私はこの特定の例を修正しようとしていません。このようなコードパターンを一般的にテストすることを求めています。ですから、「正確なエラーは何ですか?」と教えてください。私はそれをどうやって解決するかではなく、どうやって行うのかを尋ねています。

    私はこれを回避いくつかの方法を試してきた:

    • を、私は、この関数は、このオブジェクトに添付されていないと明らかに私は私のユニットテストで$scopeを使用することはできません(それは通常のメッセージExpected spy but got undefinedで終わります又は)同様の
    • IはCtrl.util = util;介してコントローラオブジェクトにそれらの機能を結合した後Ctrl.util = jasmine.createSpy()ようモックを検証するが、この場合にCtrl.utilをテスト私はを変更しよう
    • 失敗ように呼び出されていない試みがthisオブジェクトに添付し、運

    まあで、再びCtrl.utilをあざけることを、私はこの周りに自分の道を見つけることができない、私はJS忍者からいくつかの助けを期待し、作業フィドルは完璧になります。

  • 答えて

    31

    名前の範囲は公害です。あなたがしたいのは、そのロジックを別の関数に抽出して、コントローラに注入することです。すなわち

    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」。

    +1

    明確にするためには、あなたが持っているカプセル化されたロジックを挿入する必要があり、コントローラーは**これらの注入された論理アクターをテンプレートへの配信のためにスコープ変数で組み立てる責任しか負いません。 – MikeMac

    +1

    良いとは言えますが、テストしたいことは、if($ scope.flag)service.a();という単純なものです。 else service.b(); '。これだけのコンポーネントを抽出するのは自然ではありません。しかし、私はあなたの意見を得て、結局のところ、これは有効なアプローチだと思います。 –

    +5

    @ŁukaszBachmanプライベートメソッドに抽出したコードが単なる単純なもので、別の模擬可能なサービスに抽出できない場合は、 'Ctrl'の内部コード分割を無視して、このロジックを直接確認してみませんか?すなわち、あなたの例では、 'util()'の呼び出しを検証する代わりに、 'anyService.doSmth()'(そして 'util()'にあるかもしれない他のサービス呼び出し)を検証します。 –

    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(); 
    

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

    +0

    (偏っている)しかし、私はこれを "プライベートネームスペース"の下にネストすると、$ scopeまたはコントローラークラスそのものが明確になります。 –

    +1

    はい、私は同意します。 Angularのダイジェストサイクル中に評価されるべきNEEDSだけがそれに付けることができるプロジェクトワイドルールを実装しているので、私は '$ scope'に何かを添付したくありません。私は同意します。あなたのソリューションはより簡単に管理できます。 –

    41

    指定したコントローラ機能は、Angularによってコンストラクタとして使用されます。ある時点で実際のコントローラインスタンスを作成するためにnewで呼び出されます。 $スコープには公開されていないが、スパイ/スタブ/モックのために利用できる関数をコントローラオブジェクトに含める必要がある場合は、thisにそれらを割り当てることができます。

    function Ctrl($scope, anyService) { 
    
        $scope.field = "field"; 
        $scope.whenClicked = function() { 
        util(); 
        }; 
    
        this.util = function() { 
        anyService.doSmth(); 
        } 
    } 
    

    あなたが今var ctrl = new Ctrl(...)を呼び出すかCtrlインスタンスを取得するために、角度$controllerサービスを使用すると、返されるオブジェクトはutil機能が含まれます。私は別のアプローチでチャイムするつもりだhttp://jsfiddle.net/yianisn/8P9Mv/

    +1

    これは本当に良い点です! –

    +0

    これは非常に便利だった –

    +0

    +1これは、スコープに晒されたくない単一のコントローラだけが使用する小さな機能のための方法だと思います。 – Joel

    7

    現在地このアプローチを見ることができます。プライベートメソッドをテストするべきではありません。それが彼らがプライベートである理由です - それは、使用法と無関係な実装の詳細です。

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

    $scope.whenClickedが呼び出されたときに、anyService.doSmth()も呼び出す必要があります。

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

    downvoter - ご説明ください。クレームが私が質問に答えなかったならば - それは真実ではありません。プライベートメソッドをテストするには、特定のメソッドが呼び出されたわけではなく、必要な影響をテストする必要があると説明しました。 – Yehosef

    +1

    yeaはそれを説明することなく下の票を憎む:)必須説明はすべての下降声明のためにするべきである:) – Ezeewei

    +0

    'anyService.doSmith()'が呼び出されたかどうかのテストは、 'util()'がと呼ばれる。どちらもあなたのクラスの実装の詳細です。唯一の違いは、一方は外部依存性であり、他方は外部依存性ではないことです。はい、実装の詳細に依存する単体テストを作成することは脆弱ですが、それは単体テストです。 – d512

    関連する問題