webentwicklung-frage-antwort-db.com.de

Controller mit Erfolg () und Fehler () testen

Ich versuche, den besten Weg zu ermitteln, wie Erfolg und Fehlerrückrufe von Controllern in Einzeltests ausgeführt werden können. Ich kann Service-Methoden simulieren, solange der Controller nur die Standardfunktionen von $ q wie 'then' verwendet (siehe Beispiel unten). Ich habe ein Problem, wenn der Controller auf ein Versprechen "Erfolg" oder "Fehler" reagiert. (Sorry, wenn meine Terminologie nicht korrekt ist).

Hier ist ein Beispiel für einen 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);
        }
    }
]);  

Ich habe den folgenden Test

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

Der erste Test ist erfolgreich, und der zweite schlägt fehl mit dem Fehler "TypeError: Object unterstützt die Eigenschaft oder Methode 'success'" nicht. Ich bekomme das in diesem Fall, dass getDeferred.promise Keine Erfolgsfunktion hat. Okay, hier ist die Frage, wie kann man diesen Test auf nette Art und Weise schreiben, damit ich die Bedingungen für "Erfolg", "Fehler" und "Dann" eines verspotteten Dienstes testen kann?

Ich fange an zu denken, dass ich die Verwendung von success () und error () in meinen Controllern vermeiden sollte ...

EDIT

Nachdem ich nun mehr darüber nachgedacht habe und dank der ausführlichen Antwort unten, habe ich ich bin zu dem Schluss gekommen, dass die Handhabung der Rückruf- und Fehlerrückrufe im Controller schlecht ist. Wie HackedByChinese unten den \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ ' ist ein syntaktischer Zucker, der von $ http hinzugefügt wird. Wenn ich also versuche, mit dem \\ \\\\\\\\\\\\\\\\ Sorgen in meinen Controller einzugreifen, lasse ich die http-Aufrufe in einen Dienst ein. Der Ansatz, den ich verfolgen werde, ist, den Controller so zu ändern, dass er \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ "erscheinende Fehler nicht verwendet

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

Auf diese Weise kann ich die Fehler-\Erfolgsbedingungen testen, indem ich resol () und reject () für das zurückgestellte Objekt aufrufe:

'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

Wie jemand in einer gelöschten Antwort erwähnt hatte, sind success und error syntaktischer Zucker, der von $http hinzugefügt wird, sodass sie nicht vorhanden sind, wenn Sie Ihr eigenes Versprechen erstellen. Sie haben zwei Möglichkeiten:

1 - Verspotten Sie den Service nicht und verwenden Sie $httpBackend, um die Erwartungen und die Spülung festzulegen

Die Idee ist, dass Ihr myService sich so verhält, wie es normalerweise wäre, ohne zu wissen, dass es getestet wird. Mit $httpBackend können Sie Erwartungen und Antworten festlegen und löschen, sodass Sie Ihre Tests synchron abschließen können. $http wird nicht klüger sein und das Versprechen, das es zurückgibt, wird wie ein echtes aussehen und funktionieren. Diese Option ist gut, wenn Sie einfache Tests mit wenigen HTTP-Erwartungen haben.

'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 - Geben Sie ein voll verspottetes Versprechen ab

Wenn das, was Sie testen, komplizierte Abhängigkeiten aufweist und alle Einstellungen Kopfschmerzen verursachen, möchten Sie möglicherweise die Dienste und die Anrufe selbst verspotten, wie Sie es versucht haben. Der Unterschied ist, dass Sie das Versprechen vollständig verspotten möchten. Der Nachteil dabei kann die Erstellung aller möglichen Versprechungen sein. Sie können dies jedoch vereinfachen, indem Sie eine eigene Funktion zum Erstellen dieser Objekte erstellen.

Der Grund, warum dies funktioniert, liegt darin, dass wir vorgeben, dass es durch Aufrufen der von success, error oder then bereitgestellten Handler sofort aufgelöst wird, wodurch es synchron abgeschlossen wird.

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

Ich wähle selten Option 2, selbst in großen Anwendungen.

Ihre loadData- und loadData2 http-Handler haben einen Fehler. Sie verweisen auf response.data, aber die handlers werden mit den analysierten Antwortdaten direkt aufgerufen, nicht mit dem Antwortobjekt (es sollte also data anstelle von response.data sein).

26
HackedByChinese

Bedenken Sie nicht!

Die Verwendung von $httpBackend in einem Controller ist eine schlechte Idee, da Sie Bedenken in Ihrem Test vermischen. Ob Sie Daten von einem Endpunkt abrufen oder nicht, ist kein Problem des Controllers. Dies ist ein Problem des DataService, den Sie anrufen. 

Sie können dies klarer sehen, wenn Sie die Endpunkt-URL im Service ändern. Sie müssen dann beide Tests ändern: den Service-Test und den Controller-Test.

Wie bereits erwähnt, ist die Verwendung von success und error syntaktischer Zucker, und wir sollten uns auf then und catch beschränken. In der Realität kann es jedoch sein, dass Sie "alten Code" testen müssen. Dafür verwende ich diese Funktion:

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

Wenn Sie diese Funktion aufrufen, erhalten Sie ein echtes Versprechen, das bei Bedarf auf then- und catch-Methoden reagiert und auch für die success- oder error-Callbacks funktioniert. Beachten Sie, dass Erfolg und Fehler ein Versprechen selbst zurückgeben, sodass es mit verketteten then-Methoden funktioniert. 

(HINWEIS: In der vierten und sechsten Zeile gibt die Funktion die Werte zum Auflösen und Ablehnen in der data -Eigenschaft eines Objekts zurück. Dies dient dazu, das Verhalten von $ http zu verspotten, da es die Daten zurückgibt, http-Status usw.)

4
Cesar Alvarado

Ja, verwenden Sie nicht $ httpbackend in Ihrem Controller, da wir keine echten Anfragen machen müssen. Sie müssen lediglich sicherstellen, dass eine Einheit ihre Aufgabe genau wie erwartet erfüllt verstehen

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