Подтвердить что ты не робот

Протестируйте контроллер с успехом() и ошибкой()

Я пытаюсь разработать лучший способ для unit test успешных и обратных вызовов ошибок в контроллерах. Я могу издеваться над методами обслуживания, пока контроллер использует только функции $q по умолчанию, такие как "then" (см. Пример ниже). У меня возникла проблема, когда контроллер отвечает обещанию "успех" или "ошибка". (Извините, если моя терминология неверна).

Вот пример controller\service

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

Первый тест проходит, а второй терпит неудачу с ошибкой "TypeError: Object не поддерживает свойство или метод" успех ". Я получаю, что в этом случае getDeferred.promise не имеет функции успеха. Хорошо, вот вопрос, что это отличный способ написать этот тест, чтобы я мог проверить условия "успеха", "ошибки" и "затем" издевательства?

Я начинаю думать, что мне следует избегать использования success() и error() в моих контроллерах...

ИЗМЕНИТЬ

Итак, подумав об этом еще немного, и благодаря подробному ответу ниже, , я пришел к выводу, что обработка успешных и обратных вызовов ошибок в контроллере плохая. Как упоминает HackedByChinese ниже успех\ошибка - это синтаксический сахар, который добавляется через $http. Таким образом, на самом деле, пытаясь справиться с успехом \error, я разрешаю утечкам $http в мой контроллер, чего я пытаюсь избежать, обернув $http-вызовы в службе. Подход, который я собираюсь сделать, - это изменить контроллер, чтобы не использовать success\error:

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';
          });
      };
  }]);

Таким образом, я могу проверить условия ошибки \success, вызвав resol() и 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');
    });
});
4b9b3361

Ответ 1

Как упоминалось в удаленном ответе, success и error - это синтаксический сахар, добавленный $http, поэтому их нет, когда вы создаете свои собственные обещания. У вас есть два варианта:

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 - Возвратите полностью издеваемое обещание

Если вещь, которую вы тестируете, имеет сложные зависимости, и вся настройка - головная боль, вы все равно можете высмеять службы и вызовы, как вы пытались. Разница в том, что вы захотите полностью обмануть обещание. Недостатком этого может быть создание всего возможного макета promises, однако вы можете сделать это проще, создав собственную функцию для создания этих объектов.

Причина, по которой это происходит, заключается в том, что мы делаем вид, что она разрешается путем обращения к обработчикам, предоставленным success, error или 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, даже в больших приложениях.

Для чего это стоит, у ваших обработчиков http loadData и loadData2 есть ошибка. Они ссылаются на response.data, но обработчики будут вызываться непосредственно с обработанными данными ответа, а не с объектом ответа (поэтому он должен быть data вместо response.data)).

Ответ 2

Не смешивайте проблемы!

Использование $httpBackend внутри контроллера - плохая идея, поскольку вы смешиваете проблемы внутри своего теста. Независимо от того, извлекаете ли вы данные из конечной точки или нет, это не относится к контроллеру, это проблема обслуживания DataService, которое вы вызываете.

Вы можете увидеть это более четко, если вы измените URL-адрес конечной точки внутри службы, после чего вам придется изменить оба теста: тестовый сервис и тест контроллера.

Также, как упоминалось ранее, использование success и error является синтаксическим сахаром, и мы должны придерживаться использования then и catch. Но на самом деле вы можете оказаться в необходимости тестирования "устаревшего" кода. Поэтому для этого я использую эту функцию:

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. Обратите внимание, что успех и ошибка дают обещание, поэтому он будет работать с цепочками then.

(ПРИМЕЧАНИЕ. На 4-й и 6-й строках функция возвращает разрешение и отклоняет значения внутри свойства данных объекта. Это значит, что он порождает поведение $http, поскольку он возвращает данные, http Status и т.д.)

Ответ 3

Да, не используйте $httpbackend в своем контроллере, потому что нам не нужно делать реальные запросы, вам просто нужно убедиться, что один блок выполняет эту работу точно так, как ожидалось, взгляните на эти простые тесты контроллера, легко понять

/**
 * @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
            });
        });
    });
}());