Comment tester un service AngularJS avec Jasmine?

107

(Il y a une question connexe ici: le test Jasmine ne voit pas le module AngularJS )

Je veux juste tester un service sans amorcer Angular.

J'ai regardé quelques exemples et le tutoriel mais je ne vais nulle part.

Je n'ai que trois fichiers:

  • myService.js: où je définis un service AngularJS

  • test_myService.js: où je définis un test Jasmine pour le service.

  • specRunner.html: un fichier HTML avec la configuration normale de jasmine et où j'importe les deux autres fichiers précédents et les Jasmine, Angularjs et angular-mocks.js.

Voici le code du service (qui fonctionne comme prévu lorsque je ne teste pas):

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

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

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

    return serviceImplementation

});

Comme j'essaie de tester le service de manière isolée, je devrais pouvoir y accéder et vérifier leurs méthodes. Ma question est la suivante: comment puis-je injecter le service dans mon test sans amorcer AngularJS?

Par exemple, comment puis-je tester la valeur retournée pour une méthode du service avec Jasmine comme ceci:

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
la source

Réponses:

137

Le problème est que la méthode de fabrique, qui instancie le service, n'est pas appelée dans l'exemple ci-dessus (seule la création du module n'instancie pas le service).

Pour que le service soit instancié, angular.injector doit être appelé avec le module où notre service est défini. Ensuite, nous pouvons demander au nouvel objet injecteur le service et ce n'est qu'alors que le service est finalement instancié.

Quelque chose comme ça fonctionne:

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

    })

});

Une autre façon serait de passer le service à une fonction en utilisant ' invoke ':

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

    })

});

Et, enfin, la «bonne» façon de le faire est d'utiliser « inject » et « module » dans un bloc de jasmin « beforeEach ». En le faisant, nous devons réaliser que la fonction 'inject' n'est pas dans le paquet standard angularjs, mais dans le module ngMock et qu'elle ne fonctionne qu'avec jasmine.

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
la source
13
J'aimerais voir un exemple de cas où votre service a ses propres dépendances (par exemple, $ log)
Roy Truelove
2
Désolé, je cherchais en fait quelque chose comme ceci: stackoverflow.com/q/16565531/295797
Roy Truelove
1
Existe-t-il un bon moyen d'injecter le service dans un beforeEachdans le cas où beaucoup ... beaucoup ... de nombreux tests sont nécessaires pour le service? Tester un modèle de données (service) et il contient une tonne de variables globales. Merci, C§
CSS
2
Vous ne dites pas pourquoi (3) est la `` bonne façon ''
LeeGee
2
@LeeGee Je pense que nous pouvons l'appeler la manière «appropriée» car il utilise le module ngMock AngularJS qu'il est là spécifiquement à des fins de test.
Robert
5

Bien que la réponse ci-dessus fonctionne probablement très bien (je ne l'ai pas essayé :)), j'ai souvent beaucoup plus de tests à exécuter, donc je n'injecte pas les tests eux-mêmes. Je vais regrouper les cas it () en blocs describe et exécuter mon injection dans un beforeEach () ou beforeAll () dans chaque bloc describe.

Robert a également raison en ce sens qu'il dit que vous devez utiliser l'injecteur Angular $ pour informer les tests du service ou de l'usine. Angular utilise également cet injecteur dans vos applications pour indiquer à l'application ce qui est disponible. Cependant, il peut être appelé à plusieurs endroits, et il peut également être appelé implicitement au lieu de explicitement. Vous remarquerez dans mon exemple de fichier de test de spécification ci-dessous, le bloc beforeEach () appelle implicitement injector pour rendre les choses disponibles pour être affectées à l'intérieur des tests.

Pour en revenir au regroupement et à l'utilisation de blocs avant, voici un petit exemple. Je crée un service Cat et je veux le tester, donc ma configuration simple pour écrire et tester le service ressemblerait à ceci:

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

Spec: Contrôleur de chats

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

});

Spec: Service de chats

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

    })

});

EDIT Sur la base de certains des commentaires, j'ai mis à jour ma réponse pour être légèrement plus complexe, et j'ai également créé un Plunkr démontrant les tests unitaires. Plus précisément, l'un des commentaires a mentionné "Et si le service d'un contrôleur avait lui-même une simple dépendance, telle que $ log?" - qui est inclus dans l'exemple avec des cas de test. J'espère que ça aide! Testez ou piratez la planète !!!

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

RoboBear
la source
0

J'avais besoin de tester une directive qui nécessitait une autre directive, Google Places Autocomplete , je me demandais si je devais me moquer d'elle ... de toute façon, cela fonctionnait sans générer d'erreurs pour la directive qui exigeait gPlacesAutocomplete.

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
la source
-5

Si vous voulez tester un contrôleur, vous pouvez l'injecter et le tester comme ci-dessous.

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
la source
2
La question concerne le service de test, pas le contrôleur.
Bartek S