Wie teste ich einen AngularJS-Dienst mit Jasmine?

107

(Hier gibt es eine verwandte Frage: Der Jasmin-Test sieht kein AngularJS-Modul. )

Ich möchte nur einen Dienst testen, ohne Angular zu booten.

Ich habe mir einige Beispiele und das Tutorial angesehen, aber ich gehe nirgendwo hin.

Ich habe nur drei Dateien:

  • myService.js: Hier definiere ich einen AngularJS-Dienst

  • test_myService.js: Hier definiere ich einen Jasmin-Test für den Service.

  • specRunner.html: Eine HTML-Datei mit der normalen Jasmin-Konfiguration, in die ich die beiden vorherigen Dateien und die Dateien Jasmine, Angularjs und angle-mocks.js importiere.

Dies ist der Code für den Dienst (der wie erwartet funktioniert, wenn ich nicht teste):

var myModule = angular.module('myModule', []);

myModule.factory('myService', function(){

    var serviceImplementation   = {};
    serviceImplementation.one   = 1;
    serviceImplementation.two   = 2;
    serviceImplementation.three = 3;

    return serviceImplementation

});

Da ich versuche, den Dienst isoliert zu testen, sollte ich in der Lage sein, darauf zuzugreifen und ihre Methoden zu überprüfen. Meine Frage ist: Wie kann ich den Dienst in meinen Test einfügen, ohne AngularJS zu booten?

Wie kann ich beispielsweise den für eine Methode des Dienstes mit Jasmine zurückgegebenen Wert wie folgt testen:

describe('myService test', function(){
    describe('when I call myService.one', function(){
        it('returns 1', function(){
            myModule = angular.module('myModule');
                    //something is missing here..
            expect( myService.one ).toEqual(1);
        })

    })

});
Robert
quelle

Antworten:

137

Das Problem ist, dass die Factory-Methode, die den Dienst instanziiert, im obigen Beispiel nicht aufgerufen wird (nur das Erstellen des Moduls instanziiert den Dienst nicht).

Damit der Dienst instanziiert werden kann, eckiger Injektor muss mit dem Modul aufgerufen werden, in dem unser Dienst definiert ist. Dann können wir das neue Injektorobjekt für den Dienst anfordern und es ist nur dann, wenn der Dienst endgültig instanziiert wird.

So etwas funktioniert:

describe('myService test', function(){
    describe('when I call myService.one', function(){
        it('returns 1', function(){
            var $injector = angular.injector([ 'myModule' ]);
            var myService = $injector.get( 'myService' );
            expect( myService.one ).toEqual(1);
        })

    })

});

Eine andere Möglichkeit wäre, den Dienst mit ' invoke ' an eine Funktion zu übergeben :

describe('myService test', function(){
    describe('when I call myService.one', function(){
        it('returns 1', function(){

            myTestFunction = function(aService){
                expect( aService.one ).toEqual(1);
            }

            //we only need the following line if the name of the 
            //parameter in myTestFunction is not 'myService' or if
            //the code is going to be minify.
            myTestFunction.$inject = [ 'myService' ];

            var myInjector = angular.injector([ 'myModule' ]);
            myInjector.invoke( myTestFunction );
        })

    })

});

Und schließlich ist der "richtige" Weg, dies zu tun, die Verwendung von " injizieren " und " Modul " in einem "vor jedem " Jasminblock. Dabei müssen wir feststellen, dass die 'Inject'-Funktion nicht im Standard-AngularJS-Paket enthalten ist, sondern im ngMock-Modul und nur mit Jasmin funktioniert.

describe('myService test', function(){
    describe('when I call myService.one', function(){
        beforeEach(module('myModule'));
        it('returns 1', inject(function(myService){ //parameter name = service name

            expect( myService.one ).toEqual(1);

        }))

    })

});
Robert
quelle
13
Würde gerne ein Beispiel sehen, wenn Ihr Dienst eigene Abhängigkeiten hat (zB $ log)
Roy Truelove
2
Entschuldigung, ich habe tatsächlich nach so etwas gesucht: stackoverflow.com/q/16565531/295797
Roy Truelove
1
Gibt es eine gute Möglichkeit, den Service in beforeEachden Fall zu integrieren, dass viele ... viele ... viele Tests für den Service erforderlich sind? Testen eines Datenmodells (Dienstes) und es enthält eine Menge globaler Variablen. Vielen Dank, C§
CSS
2
Sie sagen nicht, warum (3) der "richtige Weg" ist
LeeGee
2
@LeeGee Ich denke, wir können es als "richtig" bezeichnen, da es das ngMock AngularJS-Modul verwendet, das es speziell zu Testzwecken gibt.
Robert
5

Während die obige Antwort wahrscheinlich gut funktioniert (ich habe es nicht ausprobiert :)), muss ich oft viel mehr Tests ausführen, damit ich selbst keine Tests einspritze. Ich werde es () Fälle in Beschreibungsblöcke gruppieren und meine Injektion in einem beforeEach () oder beforeAll () in jedem Beschreibungsblock ausführen.

Robert hat auch insofern Recht, als er sagt, dass Sie den Angular $ -Injektor verwenden müssen, um die Tests auf den Service oder die Fabrik aufmerksam zu machen. Angular verwendet diesen Injektor selbst auch in Ihren Anwendungen, um der Anwendung mitzuteilen, was verfügbar ist. Es kann jedoch an mehr als einer Stelle aufgerufen werden, und es kann auch implizit statt explizit aufgerufen werden. Sie werden feststellen, dass in meiner Beispielspezifikationstestdatei unten der Block beforeEach () implizit den Injektor aufruft , um Dinge zur Verfügung zu stellen, die innerhalb der Tests zugewiesen werden können.

Zurück zum Gruppieren von Dingen und Verwenden von Vorblöcken, hier ein kleines Beispiel. Ich mache einen Cat-Service und möchte ihn testen. Daher würde meine einfache Einrichtung zum Schreiben und Testen des Service folgendermaßen aussehen:

app.js.

var catsApp = angular.module('catsApp', ['ngMockE2E']);

angular.module('catsApp.mocks', [])
.value('StaticCatsData', function() {
  return [{
    id: 1,
    title: "Commando",
    name: "Kitty MeowMeow",
    score: 123
  }, {
    id: 2,
    title: "Raw Deal",
    name: "Basketpaws",
    score: 17
  }, {
    id: 3,
    title: "Predator",
    name: "Noseboops",
    score: 184
  }];
});

catsApp.factory('LoggingService', ['$log', function($log) {

  // Private Helper: Object or String or what passed
    // for logging? Let's make it String-readable...
  function _parseStuffIntoMessage(stuff) {
    var message = "";
    if (typeof stuff !== "string") {
      message = JSON.stringify(stuff)
    } else {
      message = stuff;
    }

    return message;
  }

  /**
   * @summary
   * Write a log statement for debug or informational purposes.
   */
  var write = function(stuff) {
    var log_msg = _parseStuffIntoMessage(stuff);
    $log.log(log_msg);
  }

  /**
   * @summary
   * Write's an error out to the console.
   */
  var error = function(stuff) {
    var err_msg = _parseStuffIntoMessage(stuff);
    $log.error(err_msg);
  }

  return {
    error: error,
    write: write
  };

}])

catsApp.factory('CatsService', ['$http', 'LoggingService', function($http, Logging) {

  /*
    response:
      data, status, headers, config, statusText
  */
  var Success_Callback = function(response) {
    Logging.write("CatsService::getAllCats()::Success!");
    return {"status": status, "data": data};
  }

  var Error_Callback = function(response) {
    Logging.error("CatsService::getAllCats()::Error!");
    return {"status": status, "data": data};
  }

  var allCats = function() {
    console.log('# Cats.allCats()');
    return $http.get('/cats')
      .then(Success_Callback, Error_Callback);
  }

  return {
    getAllCats: allCats
  };

}]);

var CatsController = function(Cats, $scope) {

  var vm = this;

  vm.cats = [];

  // ========================

  /**
   * @summary
   * Initializes the controller.
   */
  vm.activate = function() {
    console.log('* CatsCtrl.activate()!');

    // Get ALL the cats!
    Cats.getAllCats().then(
      function(litter) {
        console.log('> ', litter);
        vm.cats = litter;
        console.log('>>> ', vm.cats);
      }  
    );
  }

  vm.activate();

}
CatsController.$inject = ['CatsService', '$scope'];
catsApp.controller('CatsCtrl', CatsController);

Spezifikation: Cats Controller

'use strict';

describe('Unit Tests: Cats Controller', function() {

    var $scope, $q, deferred, $controller, $rootScope, catsCtrl, mockCatsData, createCatsCtrl;

    beforeEach(module('catsApp'));
    beforeEach(module('catsApp.mocks'));

    var catsServiceMock;

    beforeEach(inject(function(_$q_, _$controller_, $injector, StaticCatsData) {
      $q = _$q_;
      $controller = _$controller_;

      deferred = $q.defer();

      mockCatsData = StaticCatsData();

      // ToDo:
        // Put catsServiceMock inside of module "catsApp.mocks" ?
      catsServiceMock = {
        getAllCats: function() {
          // Just give back the data we expect.
          deferred.resolve(mockCatsData);
          // Mock the Promise, too, so it can run
            // and call .then() as expected
          return deferred.promise;
        }
      };
    }));


    // Controller MOCK
    var createCatsController;
    // beforeEach(inject(function (_$rootScope_, $controller, FakeCatsService) {
    beforeEach(inject(function (_$rootScope_, $controller, CatsService) {

      $rootScope = _$rootScope_;

      $scope = $rootScope.$new();
      createCatsController = function() {
          return $controller('CatsCtrl', {
              '$scope': $scope,
              CatsService: catsServiceMock
          });    
      };
    }));

    // ==========================

    it('should have NO cats loaded at first', function() {
      catsCtrl = createCatsController();

      expect(catsCtrl.cats).toBeDefined();
      expect(catsCtrl.cats.length).toEqual(0);
    });

    it('should call "activate()" on load, but only once', function() {
      catsCtrl = createCatsController();
      spyOn(catsCtrl, 'activate').and.returnValue(mockCatsData);

      // *** For some reason, Auto-Executing init functions
      // aren't working for me in Plunkr?
      // I have to call it once manually instead of relying on
      // $scope creation to do it... Sorry, not sure why.
      catsCtrl.activate();
      $rootScope.$digest();   // ELSE ...then() does NOT resolve.

      expect(catsCtrl.activate).toBeDefined();
      expect(catsCtrl.activate).toHaveBeenCalled();
      expect(catsCtrl.activate.calls.count()).toEqual(1);

      // Test/Expect additional  conditions for 
        // "Yes, the controller was activated right!"
      // (A) - there is be cats
      expect(catsCtrl.cats.length).toBeGreaterThan(0);
    });

    // (B) - there is be cats SUCH THAT
      // can haz these properties...
    it('each cat will have a NAME, TITLE and SCORE', function() {
      catsCtrl = createCatsController();
      spyOn(catsCtrl, 'activate').and.returnValue(mockCatsData);

      // *** and again...
      catsCtrl.activate();
      $rootScope.$digest();   // ELSE ...then() does NOT resolve.

      var names = _.map(catsCtrl.cats, function(cat) { return cat.name; })
      var titles = _.map(catsCtrl.cats, function(cat) { return cat.title; })
      var scores = _.map(catsCtrl.cats, function(cat) { return cat.score; })

      expect(names.length).toEqual(3);
      expect(titles.length).toEqual(3);
      expect(scores.length).toEqual(3); 
    });

});

Spezifikation: Cats Service

'use strict';

describe('Unit Tests: Cats Service', function() {

  var $scope, $rootScope, $log, cats, logging, $httpBackend, mockCatsData;

  beforeEach(module('catsApp'));
  beforeEach(module('catsApp.mocks'));

  describe('has a method: getAllCats() that', function() {

    beforeEach(inject(function($q, _$rootScope_, _$httpBackend_, _$log_, $injector, StaticCatsData) {
      cats = $injector.get('CatsService');
      $rootScope = _$rootScope_;
      $httpBackend = _$httpBackend_;

      // We don't want to test the resolving of *actual data*
      // in a unit test.
      // The "proper" place for that is in Integration Test, which
      // is basically a unit test that is less mocked - you test
      // the endpoints and responses and APIs instead of the
      // specific service behaviors.
      mockCatsData = StaticCatsData();

      // For handling Promises and deferrals in our Service calls...
      var deferred = $q.defer();
      deferred.resolve(mockCatsData); //  always resolved, you can do it from your spec

      // jasmine 2.0
        // Spy + Promise Mocking
        // spyOn(obj, 'method'), (assumes obj.method is a function)
      spyOn(cats, 'getAllCats').and.returnValue(deferred.promise);

      /*
        To mock $http as a dependency, use $httpBackend to
        setup HTTP calls and expectations.
      */
      $httpBackend.whenGET('/cats').respond(200, mockCatsData);
    }));

    afterEach(function() {
      $httpBackend.verifyNoOutstandingExpectation();
      $httpBackend.verifyNoOutstandingRequest();
    })

    it(' exists/is defined', function() {
      expect( cats.getAllCats ).toBeDefined();
      expect( typeof cats.getAllCats ).toEqual("function");
    });

    it(' returns an array of Cats, where each cat has a NAME, TITLE and SCORE', function() {
      cats.getAllCats().then(function(data) {
        var names = _.map(data, function(cat) { return cat.name; })
        var titles = _.map(data, function(cat) { return cat.title; })
        var scores = _.map(data, function(cat) { return cat.score; })

        expect(names.length).toEqual(3);
        expect(titles.length).toEqual(3);
        expect(scores.length).toEqual(3);
      })
    });

  })

  describe('has a method: getAllCats() that also logs', function() {

      var cats, $log, logging;

      beforeEach(inject(
        function(_$log_, $injector) {
          cats = $injector.get('CatsService');
          $log = _$log_;
          logging = $injector.get('LoggingService');

          spyOn(cats, 'getAllCats').and.callThrough();
        }
      ))

      it('that on SUCCESS, $logs to the console a success message', function() {
        cats.getAllCats().then(function(data) {
          expect(logging.write).toHaveBeenCalled();
          expect( $log.log.logs ).toContain(["CatsService::getAllCats()::Success!"]);
        })
      });

    })

});

BEARBEITEN Basierend auf einigen Kommentaren habe ich meine Antwort etwas komplexer aktualisiert und mir auch einen Plunkr ausgedacht, der Unit-Tests demonstriert. In einem der Kommentare wurde insbesondere erwähnt: "Was ist, wenn der Dienst eines Controllers selbst eine einfache Abhängigkeit aufweist, z. B. $ log?" - was im Beispiel mit Testfällen enthalten ist. Ich hoffe es hilft! Teste oder hacke den Planeten !!!

https://embed.plnkr.co/aSPHnr/

RoboBear
quelle
0

Ich musste eine Direktive testen, für die eine andere Direktive erforderlich war, Google Places Autocomplete . Ich überlegte , ob ich sie nur verspotten sollte. Trotzdem funktionierte dies ohne Fehler für die Direktive, für die gPlacesAutocomplete erforderlich war.

describe('Test directives:', function() {
    beforeEach(module(...));
    beforeEach(module(...));
    beforeEach(function() {
        angular.module('google.places', [])
        .directive('gPlacesAutocomplete',function() {
            return {
                require: ['ngModel'],
                restrict: 'A',
                scope:{},
                controller: function() { return {}; }
             };
        });
     });
     beforeEach(module('google.places'));
});
Jerinaw
quelle
-5

Wenn Sie einen Controller testen möchten, können Sie ihn wie folgt injizieren und testen.

describe('When access Controller', function () {
    beforeEach(module('app'));

    var $controller;

    beforeEach(inject(function (_$controller_) {
        // The injector unwraps the underscores (_) from around the parameter names when matching
        $controller = _$controller_;
    }));

    describe('$scope.objectState', function () {
        it('is saying hello', function () {
            var $scope = {};
            var controller = $controller('yourController', { $scope: $scope });
            expect($scope.objectState).toEqual('hello');
        });
    });
});
Lazaro Fernandes Lima Suleiman
quelle
2
Frage ist über Testdienst, nicht Controller.
Bartek S