¿Cómo pruebo un servicio AngularJS con Jasmine?

107

(Hay una pregunta relacionada aquí: la prueba Jasmine no ve el módulo AngularJS )

Solo quiero probar un servicio sin iniciar Angular.

He visto algunos ejemplos y el tutorial, pero no me voy a ninguna parte.

Solo tengo tres archivos:

  • myService.js: donde defino un servicio AngularJS

  • test_myService.js: donde defino una prueba de Jasmine para el servicio.

  • specRunner.html: un archivo HTML con la configuración normal de jazmín y donde importo los otros dos archivos anteriores y Jasmine, Angularjs y angular-mocks.js.

Este es el código para el servicio (que funciona como se esperaba cuando no estoy probando):

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

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

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

    return serviceImplementation

});

Como estoy tratando de probar el servicio de forma aislada, debería poder acceder a él y verificar sus métodos. Mi pregunta es: ¿cómo puedo inyectar el servicio en mi prueba sin iniciar AngularJS?

Por ejemplo, ¿cómo puedo probar el valor devuelto para un método del servicio con Jasmine como este?

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
fuente

Respuestas:

137

El problema es que el método de fábrica, que crea una instancia del servicio, no se llama en el ejemplo anterior (solo crear el módulo no crea una instancia del servicio).

Para instanciar el servicio angular.injector debe ser llamado con el módulo donde se define nuestro servicio. Luego, podemos preguntarle al nuevo objeto inyector por el servicio y es solo entonces cuando el servicio finalmente se instancia.

Algo como esto funciona:

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

    })

});

Otra forma sería pasar el servicio a una función usando ' 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 );
        })

    })

});

Y, finalmente, la forma 'correcta' de hacerlo es usando ' inyectar ' y ' módulo ' en un bloque jazmín ' beforeEach '. Al hacerlo tenemos que darnos cuenta de que la función 'inyectar' no está en el paquete angularjs estándar, sino en el módulo ngMock y que solo funciona con 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
fuente
13
Me encantaría ver un ejemplo de cuándo su servicio tiene dependencias propias (por ejemplo, $ log)
Roy Truelove
2
Lo siento, en realidad estaba buscando algo como esto: stackoverflow.com/q/16565531/295797
Roy Truelove
1
¿Existe una buena manera de inyectar el servicio en un beforeEachen el caso de que sean necesarias muchas ... muchas ... muchas pruebas para el servicio? Probando un modelo de datos (servicio) y contiene un montón de variables globales. Gracias, C§
CSS
2
No dice por qué (3) es la 'manera correcta'
LeeGee
2
@LeeGee Creo que podemos llamarlo de la manera 'adecuada' porque usa el módulo ngMock AngularJS que está allí específicamente para fines de prueba.
Robert
5

Si bien la respuesta anterior probablemente funcione bien (no la he probado :)), a menudo tengo muchas más pruebas para ejecutar, así que no inyecto en las pruebas. Agruparé () los casos en bloques de descripción y ejecutaré mi inyección en un beforeEach () o beforeAll () en cada bloque de descripción.

Robert también tiene razón en que dice que debe usar el inyector Angular $ para que las pruebas conozcan el servicio o la fábrica. Angular también usa este inyector en sus aplicaciones para decirle a la aplicación qué está disponible. Sin embargo, se puede llamar en más de un lugar y también se puede llamar implícitamente en lugar de explícitamente. Notará que en mi archivo de prueba de especificaciones de ejemplo a continuación, el bloque beforeEach () llama implícitamente al inyector para que las cosas estén disponibles para ser asignadas dentro de las pruebas.

Volviendo a agrupar cosas y usar bloques anteriores, aquí hay un pequeño ejemplo. Estoy creando un Servicio Cat y quiero probarlo, por lo que mi configuración simple para escribir y probar el Servicio se vería así:

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

Especificaciones: Controlador de gatos

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

});

Especificaciones: Servicio de gatos

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

    })

});

EDITAR Basado en algunos de los comentarios, he actualizado mi respuesta para que sea un poco más compleja, y también he creado un Plunkr demostrando pruebas unitarias. Específicamente, uno de los comentarios mencionó "¿Qué pasa si el Servicio de un controlador tiene en sí mismo una dependencia simple, como $ log?" - que se incluye en el ejemplo con casos de prueba. ¡Espero eso ayude! ¡Prueba o piratea el planeta!

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

RoboBear
fuente
0

Necesitaba probar una directiva que requería otra directiva, Google Places Autocomplete , estaba debatiendo si debería simplemente burlarme de ella ... de todos modos esto funcionó sin arrojar ningún error para la directiva que requería 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
fuente
-5

Si desea probar un controlador, puede inyectarlo y probarlo como se muestra a continuación.

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');
        });
    });
});
Lázaro Fernandes Lima Suleiman
fuente
2
La pregunta es sobre el servicio de prueba, no sobre el controlador.
Bartek S