web-dev-qa-db-fra.com

Tester un contrôleur avec succès () et error ()

J'essaie de trouver le meilleur moyen de réussir le test des unités et de rappeler les erreurs dans les contrôleurs. Je peux simuler des méthodes de service tant que le contrôleur utilise uniquement les fonctions $ q par défaut telles que 'then' (voir l'exemple ci-dessous). Je rencontre un problème lorsque le contrôleur répond à une promesse de "succès" ou "d'erreur". (Désolé si ma terminologie n'est pas correcte).

Voici un exemple de contrôleur\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);
        }
    }
]);  

J'ai le test suivant

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

Le premier test réussit et le second échoue avec l'erreur "TypeError: l'objet ne prend pas en charge la propriété ou la méthode 'success'". Je comprends que dans cet exemple, getDeferred.promise N'a pas de fonction de succès. Ok, voici la question, quel est un bon moyen d'écrire ce test afin que je puisse tester les conditions de "succès", "erreur" et "puis" d'un service simulé?

Je commence à penser que je devrais éviter l'utilisation de success () et error () dans mes contrôleurs ...

MODIFIER

Donc, après avoir réfléchi un peu plus, et grâce à la réponse détaillée ci-dessous, je suis parvenu à la conclusion que la gestion des rappels de succès et d’erreur dans le contrôleur est mauvaise. Comme HackedByChinese mentionne ci-dessous success\error est le sucre syntaxique ajouté par $ http. Donc, en réalité, en essayant de gérer le succès\erreur, je laisse les préoccupations de $ http se fuir dans mon contrôleur, ce qui est exactement ce que j'essayais d'éviter en encapsulant les appels $ http dans un service. L’approche que je vais adopter consiste à changer le contrôleur pour ne pas utiliser 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';
          });
      };
  }]);

De cette manière, je peux tester les conditions d'erreur\succès en appelant resol () et rejette () sur l'objet différé:

'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

Comme quelqu'un l'a mentionné dans une réponse supprimée, success et error sont des sucres syntaxiques ajoutés par $http afin qu'ils ne soient pas là lorsque vous créez votre propre promesse. Vous avez deux options:

1 - Ne vous moquez pas du service et utilisez $httpBackend pour définir les attentes et vider

L'idée est de laisser votre myService agir comme il le ferait normalement sans savoir que le test est en cours. $httpBackend vous permettra de définir les attentes et les réponses et de les vider afin de pouvoir effectuer vos tests de manière synchrone. $http ne sera pas plus sage et la promesse qu’elle renverra ressemblera et fonctionnera comme une vraie. Cette option est utile si vous avez des tests simples avec peu d'attentes 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 - Retourner une promesse complètement moquée

Si la chose que vous testez a des dépendances compliquées et que toute la configuration est un casse-tête, vous pouvez toujours vouloir vous moquer des services et des appels eux-mêmes comme vous l'avez tenté. La différence est que vous voudrez faire une fausse promesse. L'inconvénient de ceci peut être de créer toutes les fausses promesses possibles, mais vous pouvez le faciliter en créant votre propre fonction pour créer ces objets.

Cela fonctionne parce que nous prétendons résoudre le problème en appelant immédiatement les gestionnaires fournis par success, error ou then, ce qui entraîne son achèvement de manière synchrone.

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

Je choisis rarement l'option 2, même dans les grandes applications.

Pour ce que ça vaut, vos gestionnaires loadData et loadData2 http ont une erreur. Ils référencent response.data mais les handlers seront appelés avec les données de réponse analysées directement, pas avec l'objet de réponse (il devrait donc être data au lieu de response.data).

26
HackedByChinese

Ne mélangez pas les préoccupations!

Utiliser $httpBackend dans un contrôleur est une mauvaise idée car vous mélangez les problèmes dans votre test. Que vous récupériez des données d'un point de terminaison ou non n'est pas une préoccupation du contrôleur, cela concerne le DataService que vous appelez. 

Vous pouvez le voir plus clairement si vous modifiez l'URL du point de terminaison dans le service, vous devrez alors modifier les deux tests: le test de service et le test de contrôleur.

De plus, comme mentionné précédemment, l'utilisation de success et error est un sucre syntaxique et nous devrions nous en tenir à l'utilisation de then et catch. Mais en réalité, vous pouvez avoir besoin de tester du code "hérité". Donc pour cela j'utilise cette fonction:

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

En appelant cette fonction, vous obtiendrez une véritable promesse qui répondra aux méthodes then et catch lorsque vous en aurez besoin et fonctionnera également pour les rappels success ou error. Notez que le succès et l'erreur renvoient une promesse elle-même afin qu'elle fonctionne avec les méthodes chaînées then

(REMARQUE: Sur les lignes 4 et 6, la fonction retourne les valeurs de résolution et de rejet de la propriété data d'un objet. Il s'agit de simuler le comportement de $ http puisqu'il renvoie les données, l'état http, etc.)

4
Cesar Alvarado

Oui, n'utilisez pas $ httpbackend dans votre contrôleur, car nous n'avons pas besoin de faire de véritables requêtes, vous devez simplement vous assurer qu'une unité effectue son travail exactement comme prévu. Jetez un coup d'œil à ces tests simples, comprendre

/**
 * @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
Mohammed Ramadan