web-dev-qa-db-ja.com

コントローラーを成功()およびエラー()でテストする

コントローラで成功とエラーのコールバックを単体テストするための最良の方法を模索しています。コントローラが 'then'などのデフォルトの$ q関数のみを使用している限り、サービスメソッドをモックアウトできます(以下の例を参照)。コントローラーが「成功」または「エラー」の約束に応答すると問題が発生します。 (私の用語が正しくない場合は申し訳ありません)。

これはコントローラ\サービスの例です

var myControllers = angular.module('myControllers');

myControllers.controller('SimpleController', ['$scope', 'myService',
  function ($scope, myService) {

      var id = 1;
      $scope.loadData = function () {
          myService.get(id).then(function (response) {
              $scope.data = response.data;
          });
      };

      $scope.loadData2 = function () {
          myService.get(id).success(function (response) {
              $scope.data = response.data;
          }).error(function(response) {
              $scope.error = 'ERROR';
          });
      }; 
  }]);


cocoApp.service('myService', [
    '$http', function($http) {
        function get(id) {
            return $http.get('/api/' + id);
        }
    }
]);  

次のテストがあります

'use strict';

describe('SimpleControllerTests', function () {

    var scope;
    var controller;
    var getResponse = { data: 'this is a mocked response' };

    beforeEach(angular.mock.module('myApp'));

    beforeEach(angular.mock.inject(function($q, $controller, $rootScope, $routeParams){

        scope = $rootScope;
        var myServiceMock = {
            get: function() {}
        };

        // setup a promise for the get
        var getDeferred = $q.defer();
        getDeferred.resolve(getResponse);
        spyOn(myServiceMock, 'get').andReturn(getDeferred.promise);

        controller = $controller('SimpleController', { $scope: scope, myService: myServiceMock });
    }));


    it('this tests works', function() {
        scope.loadData();
        expect(scope.data).toEqual(getResponse.data);
    });

    it('this doesnt work', function () {
        scope.loadData2();
        expect(scope.data).toEqual(getResponse.data);
    });
});

最初のテストは成功し、2番目のテストはエラー「TypeError:Object does not support property or method 'success'」で失敗します。この例では、getDeferred.promiseに成功関数がないことがわかります。さて、ここで質問です。偽のサービスの「成功」、「エラー」、および「その後」の状態をテストできるように、このテストを作成する良い方法は何ですか?

コントローラでのsuccess()およびerror()の使用を避けるべきだと私は考え始めています...

[〜#〜]編集[〜#〜]

したがって、これについてもう少し考えた後、以下の詳細な回答のおかげでコントローラでの成功とエラーのコールバックの処理が悪いという結論に達しました HackedByChineseが以下の成功について言及しているように\エラーは、$ httpによって追加される構文糖です。したがって、実際には、成功\エラーを処理しようとすることで、$ httpの懸念をコントローラーにリークさせています。これは、$ http呼び出しをサービスにラップすることで回避しようとしていたこととまったく同じです。私が取ろうとしているアプローチは、成功\エラーを使用しないようにコントローラを変更することです:

myControllers.controller('SimpleController', ['$scope', 'myService',
  function ($scope, myService) {

      var id = 1;
      $scope.loadData = function () {
          myService.get(id).then(function (response) {
              $scope.data = response.data;
          }, function (response) {
              $scope.error = 'ERROR';
          });
      };
  }]);

このように、遅延オブジェクトでresolve()およびreject()を呼び出すことにより、エラー\成功条件をテストできます。

'use strict';

describe('SimpleControllerTests', function () {

    var scope;
    var controller;
    var getResponse = { data: 'this is a mocked response' };
    var getDeferred;
    var myServiceMock;

    //mock Application to allow us to inject our own dependencies
    beforeEach(angular.mock.module('myApp'));
    //mock the controller for the same reason and include $rootScope and $controller
    beforeEach(angular.mock.inject(function($q, $controller, $rootScope, $routeParams) {

        scope = $rootScope;
        myServiceMock = {
            get: function() {}
        };
        // setup a promise for the get
        getDeferred = $q.defer();
        spyOn(myServiceMock, 'get').andReturn(getDeferred.promise);
        controller = $controller('SimpleController', { $scope: scope, myService: myServiceMock });  
    }));

    it('should set some data on the scope when successful', function () {
        getDeferred.resolve(getResponse);
        scope.loadData();
        scope.$apply();
        expect(myServiceMock.get).toHaveBeenCalled();
        expect(scope.data).toEqual(getResponse.data);
    });

    it('should do something else when unsuccessful', function () {
        getDeferred.reject(getResponse);
        scope.loadData();
        scope.$apply();
        expect(myServiceMock.get).toHaveBeenCalled();
        expect(scope.error).toEqual('ERROR');
    });
});
22
nixon

誰かが削除された回答で述べたように、successerror$httpによって追加された構文上の砂糖であるため、独自のプロミスを作成するときには存在しません。次の2つのオプションがあります。

1-サービスをモックしないで、$httpBackendを使用して期待値を設定し、フラッシュします

アイデアは、テストされていることを知らずにmyServiceを通常どおりに動作させることです。 $httpBackendを使用すると、期待と応答を設定し、それらをフラッシュして、テストを同期的に完了することができます。 $httpは賢くならず、それが返すプロミスは実際のものと同じように見え、機能します。このオプションは、HTTPの期待がほとんどない単純なテストがある場合に適しています。

'use strict';

describe('SimpleControllerTests', function () {

    var scope;
    var expectedResponse = { name: 'this is a mocked response' };
    var $httpBackend, $controller;

    beforeEach(module('myApp'));

    beforeEach(inject(function(_$rootScope_, _$controller_, _$httpBackend_){ 
        // the underscores are a convention ng understands, just helps us differentiate parameters from variables
        $controller = _$controller_;
        $httpBackend = _$httpBackend_;
        scope = _$rootScope_;
    }));

    // makes sure all expected requests are made by the time the test ends
    afterEach(function() {
      $httpBackend.verifyNoOutstandingExpectation();
      $httpBackend.verifyNoOutstandingRequest();
    });

    describe('should load data successfully', function() {

        beforeEach(function() {
           $httpBackend.expectGET('/api/1').response(expectedResponse);
           $controller('SimpleController', { $scope: scope });

           // causes the http requests which will be issued by myService to be completed synchronously, and thus will process the fake response we defined above with the expectGET
           $httpBackend.flush();
        });

        it('using loadData()', function() {
          scope.loadData();
          expect(scope.data).toEqual(expectedResponse);
        });

        it('using loadData2()', function () {
          scope.loadData2();
          expect(scope.data).toEqual(expectedResponse);
        });
    });

    describe('should fail to load data', function() {
        beforeEach(function() {
           $httpBackend.expectGET('/api/1').response(500); // return 500 - Server Error
           $controller('SimpleController', { $scope: scope });
           $httpBackend.flush();
        });

        it('using loadData()', function() {
          scope.loadData();
          expect(scope.error).toEqual('ERROR');
        });

        it('using loadData2()', function () {
          scope.loadData2();
          expect(scope.error).toEqual('ERROR');
        });
    });           
});

2-完全に偽造された約束を返す

テストするものに複雑な依存関係があり、すべての設定が頭痛の種である場合でも、試行したとおりにサービスと呼び出し自体を模擬したい場合があります。違いは、約束を完全にあざける必要があるということです。これの欠点は、可能なすべての模擬プロミスを作成することですが、これらのオブジェクトを作成するための独自の関数を作成することで、これを簡単にすることができます。

これが機能する理由は、successerror、またはthenによって提供されるハンドラーをすぐに呼び出して解決し、同期して完了するように装うためです。

'use strict';

describe('SimpleControllerTests', function () {

    var scope;
    var expectedResponse = { name: 'this is a mocked response' };
    var $controller, _mockMyService, _mockPromise = null;

    beforeEach(module('myApp'));

    beforeEach(inject(function(_$rootScope_, _$controller_){ 
        $controller = _$controller_;
        scope = _$rootScope_;

        _mockMyService = {
            get: function() {
               return _mockPromise;
            }
        };
    }));

    describe('should load data successfully', function() {

        beforeEach(function() {

          _mockPromise = {
             then: function(successFn) {
               successFn(expectedResponse);
             },
             success: function(fn) {
               fn(expectedResponse);
             }
          };

           $controller('SimpleController', { $scope: scope, myService: _mockMyService });
        });

        it('using loadData()', function() {
          scope.loadData();
          expect(scope.data).toEqual(expectedResponse);
        });

        it('using loadData2()', function () {
          scope.loadData2();
          expect(scope.data).toEqual(expectedResponse);
        });
    });

    describe('should fail to load data', function() {
        beforeEach(function() {
          _mockPromise = {
            then: function(successFn, errorFn) {
              errorFn();
            },
            error: function(fn) {
              fn();
            }
          };

          $controller('SimpleController', { $scope: scope, myService: _mockMyService });
        });

        it('using loadData()', function() {
          scope.loadData();
          expect(scope.error).toEqual("ERROR");
        });

        it('using loadData2()', function () {
          scope.loadData2();
          expect(scope.error).toEqual("ERROR");
        });
    });           
});

大きなアプリケーションであっても、オプション2を選択することはめったにありません。

価値があるのは、loadDataおよびloadData2 httpハンドラーにエラーがあることです。それらはresponse.dataを参照しますが、 handlers は、応答オブジェクトではなく、解析された応答データで直接呼び出されます(したがって、response.dataではなくdataである必要があります) 。

26
moribvndvs

心配事を混ぜないでください!

コントローラー内で$httpBackendを使用するのは悪い考えです。テスト内で懸念事項を混ぜ合わせているからです。エンドポイントからデータを取得するかどうかは、コントローラーの問題ではなく、呼び出すDataServiceの問題です。

サービス内のエンドポイントURLを変更すると、サービステストとコントローラーテストの両方のテストを変更する必要があるため、これをより明確に確認できます。

また、前述のように、successerrorの使用は構文上の砂糖であり、thencatchの使用に固執する必要があります。しかし実際には、「レガシー」コードをテストする必要があることに気付くかもしれません。そのため、私はこの関数を使用しています:

function generatePromiseMock(resolve, reject) {
    var promise;
    if(resolve) {
        promise = q.when({data: resolve});
    } else if (reject){
        promise = q.reject({data: reject});
    } else {
        throw new Error('You need to provide an argument');
    }
    promise.success = function(fn){
        return q.when(fn(resolve));
    };
    promise.error = function(fn) {
        return q.when(fn(reject));
    };
    return promise;
}

この関数を呼び出すことにより、必要なときにthenおよびcatchメソッドに応答し、successまたはerrorでも機能するという真の約束が得られますコールバック。成功とエラーはpromise自体を返すため、チェーンされたthenメソッドで動作することに注意してください。

(注:4行目と6行目で、関数はオブジェクトのdataプロパティ内のresolveとrejectの値を返します。これは、データやhttpステータスなどを返すため、$ httpの動作を模擬するためです。)

4
Cesar Alvarado

はい、コントローラで$ httpbackendを使用しないでください。実際のリクエストを行う必要がないため、1つのユニットが期待どおりに機能していることを確認し、この単純なコントローラテストを確認するだけで簡単です。理解する

/**
 * @description Tests for adminEmployeeCtrl controller
 */
(function () {

    "use strict";

    describe('Controller: adminEmployeeCtrl ', function () {

        /* jshint -W109 */
        var $q, $scope, $controller;
        var empService;
        var errorResponse = 'Not found';


        var employeesResponse = [
            {id:1,name:'mohammed' },
            {id:2,name:'ramadan' }
        ];

        beforeEach(module(
            'loadRequiredModules'
        ));

        beforeEach(inject(function (_$q_,
                                    _$controller_,
                                    _$rootScope_,
                                    _empService_) {
            $q = _$q_;
            $controller = _$controller_;
            $scope = _$rootScope_.$new();
            empService = _empService_;
        }));

        function successSpies(){

            spyOn(empService, 'findEmployee').and.callFake(function () {
                var deferred = $q.defer();
                deferred.resolve(employeesResponse);
                return deferred.promise;
                // shortcut can be one line
                // return $q.resolve(employeesResponse);
            });
        }

        function rejectedSpies(){
            spyOn(empService, 'findEmployee').and.callFake(function () {
                var deferred = $q.defer();
                deferred.reject(errorResponse);
                return deferred.promise;
                // shortcut can be one line
                // return $q.reject(errorResponse);
            });
        }

        function initController(){

            $controller('adminEmployeeCtrl', {
                $scope: $scope,
                empService: empService
            });
        }


        describe('Success controller initialization', function(){

            beforeEach(function(){

                successSpies();
                initController();
            });

            it('should findData by calling findEmployee',function(){
                $scope.findData();
                // calling $apply to resolve deferred promises we made in the spies
                $scope.$apply();
                expect($scope.loadingEmployee).toEqual(false);
                expect($scope.allEmployees).toEqual(employeesResponse);
            });
        });

        describe('handle controller initialization errors', function(){

            beforeEach(function(){

                rejectedSpies();
                initController();
            });

            it('should handle error when calling findEmployee', function(){
                $scope.findData();
                $scope.$apply();
                // your error expectations
            });
        });
    });
}());
0