Inyectando un simulacro en un servicio AngularJS

114

Tengo un servicio AngularJS escrito y me gustaría probarlo por unidad.

angular.module('myServiceProvider', ['fooServiceProvider', 'barServiceProvider']).
    factory('myService', function ($http, fooService, barService) {

    this.something = function() {
        // Do something with the injected services
    };

    return this;
});

Mi archivo app.js tiene estos registrados:

angular
.module('myApp', ['fooServiceProvider','barServiceProvider','myServiceProvider']
)

Puedo probar que el DI funciona como tal:

describe("Using the DI framework", function() {
    beforeEach(module('fooServiceProvider'));
    beforeEach(module('barServiceProvider'));
    beforeEach(module('myServiceProvder'));

    var service;

    beforeEach(inject(function(fooService, barService, myService) {
        service=myService;
    }));

    it("can be instantiated", function() {
        expect(service).not.toBeNull();
    });
});

Esto demostró que el servicio puede ser creado por el marco DI, sin embargo, a continuación quiero probar el servicio unitario, lo que significa simular los objetos inyectados.

¿Cómo voy a hacer esto?

Intenté poner mis objetos simulados en el módulo, por ejemplo

beforeEach(module(mockNavigationService));

y reescribiendo la definición de servicio como:

function MyService(http, fooService, barService) {
    this.somthing = function() {
        // Do something with the injected services
    };
});

angular.module('myServiceProvider', ['fooServiceProvider', 'barServiceProvider']).
    factory('myService', function ($http, fooService, barService) { return new MyService($http, fooService, barService); })

Pero esto último parece detener el servicio creado por la DI como todos.

¿Alguien sabe cómo puedo burlarme de los servicios inyectados para mis pruebas unitarias?

Gracias

David

BanksySan
fuente
Puedes echar un vistazo a esta respuesta mía a otra pregunta, espero que te sea de ayuda.
remigio

Respuestas:

183

Puede inyectar simulacros en su servicio usando $provide.

Si tiene el siguiente servicio con una dependencia que tiene un método llamado getSomething:

angular.module('myModule', [])
  .factory('myService', function (myDependency) {
        return {
            useDependency: function () {
                return myDependency.getSomething();
            }
        };
  });

Puede inyectar una versión simulada de myDependency de la siguiente manera:

describe('Service: myService', function () {

  var mockDependency;

  beforeEach(module('myModule'));

  beforeEach(function () {

      mockDependency = {
          getSomething: function () {
              return 'mockReturnValue';
          }
      };

      module(function ($provide) {
          $provide.value('myDependency', mockDependency);
      });

  });

  it('should return value from mock dependency', inject(function (myService) {
      expect(myService.useDependency()).toBe('mockReturnValue');
  }));

});

Tenga en cuenta que debido a la llamada a $provide.value usted, en realidad no es necesario inyectar explícitamente myDependency en ningún lugar. Ocurre bajo el capó durante la inyección de myService. Al configurar mockDependency aquí, fácilmente podría ser un espía.

Gracias a loyalBrown por el enlace a ese gran video .

Juan Galambos
fuente
13
Funciona muy bien, pero cuidado con un detalle: ¡la beforeEach(module('myModule'));llamada TIENE QUE llegar antes que la beforeEach(function () { MOCKING })llamada, o de lo contrario los servicios reales sobrescribirán las simulaciones!
Nikos Paraskevopoulos
1
¿Hay alguna manera de burlarse del servicio pero de ser constante de la misma manera?
Artem
5
Similar al comentario de Nikos, cualquier $providellamada debe realizarse antes de usar $injector, de lo contrario, recibirá un error:Injector already created, can not register a module!
providencemac
7
¿Qué pasa si tu simulacro necesita $ q? Entonces no puede inyectar $ q en el simulacro antes de llamar a module () para registrar el simulacro. ¿Alguna idea?
Jake
4
Si está usando coffeescript y está viendo Error: [ng:areq] Argument 'fn' is not a function, got Object, asegúrese de poner un returnen la línea después $provide.value(...). Regresar implícitamente me $provide.value(...)causó ese error.
yndolok
4

Desde mi punto de vista, no hay necesidad de burlarse de los servicios en sí. Simplemente burlarse de las funciones del servicio. De esa manera, puede hacer que angular inyecte sus servicios reales como lo hace en toda la aplicación. Luego, simule las funciones en el servicio según sea necesario usando la spyOnfunción de Jasmine .

Ahora, si el servicio en sí es una función, y no un objeto con el que puede usar spyOn, hay otra forma de hacerlo. Necesitaba hacer esto y encontré algo que funciona bastante bien para mí. Consulte ¿Cómo se simula el servicio Angular que es una función?

dnc253
fuente
3
No creo que esto responda a la pregunta. ¿Qué pasa si la fábrica del servicio que se está burlando hace algo no trivial, como golpear el servidor para obtener datos? Esa sería una buena razón para querer burlarse de ella. Desea evitar la llamada al servidor y en su lugar crear una versión simulada del servicio con datos falsos. Burlarse de $ http tampoco es una buena solución, porque en realidad está probando dos servicios en una prueba, en lugar de probar los dos servicios por separado. Por eso me gustaría repetir la pregunta. ¿Cómo se pasa un servicio simulado a otro servicio en una prueba unitaria?
Patrick Arnesen
1
Si le preocupa que el servicio llegue al servidor en busca de datos, para eso es $ httpBackend ( docs.angularjs.org/api/ngMock.$httpBackend ). No estoy seguro de qué otra cosa sería una preocupación en la fábrica del servicio que requeriría burlarse de todo el servicio.
dnc253
2

Otra opción para facilitar la simulación de dependencias en Angular y Jasmine es utilizar QuickMock. Se puede encontrar en GitHub y te permite crear simulacros simples de forma reutilizable. Puede clonarlo desde GitHub a través del siguiente enlace. El archivo README se explica por sí mismo, pero es de esperar que pueda ayudar a otros en el futuro.

https://github.com/tennisgent/QuickMock

describe('NotificationService', function () {
    var notificationService;

    beforeEach(function(){
        notificationService = QuickMock({
            providerName: 'NotificationService', // the provider we wish to test
            moduleName: 'QuickMockDemo',         // the module that contains our provider
            mockModules: ['QuickMockDemoMocks']  // module(s) that contains mocks for our provider's dependencies
        });
    });
    ....

Gestiona automáticamente todo el código repetitivo mencionado anteriormente, por lo que no tiene que escribir todo ese código de inyección simulado en cada prueba. Espero que ayude.

tenista
fuente
2

Además de la respuesta de John Galambos : si solo desea simular métodos específicos de un servicio, puede hacerlo así:

describe('Service: myService', function () {

  var mockDependency;

  beforeEach(module('myModule'));

  beforeEach(module(function ($provide, myDependencyProvider) {
      // Get an instance of the real service, then modify specific functions
      mockDependency = myDependencyProvider.$get();
      mockDependency.getSomething = function() { return 'mockReturnValue'; };
      $provide.value('myDependency', mockDependency);
  });

  it('should return value from mock dependency', inject(function (myService) {
      expect(myService.useDependency()).toBe('mockReturnValue');
  }));

});
Encendedor
fuente
1

Si su controlador está escrito para tomar una dependencia como esta:

app.controller("SomeController", ["$scope", "someDependency", function ($scope, someDependency) {
    someDependency.someFunction();
}]);

entonces puedes hacer una falsificación someDependencyen una prueba de Jasmine como esta:

describe("Some Controller", function () {

    beforeEach(module("app"));


    it("should call someMethod on someDependency", inject(function ($rootScope, $controller) {
        // make a fake SomeDependency object
        var someDependency = {
            someFunction: function () { }
        };

        spyOn(someDependency, "someFunction");

        // this instantiates SomeController, using the passed in object to resolve dependencies
        controller("SomeController", { $scope: scope, someDependency: someDependency });

        expect(someDependency.someFunction).toHaveBeenCalled();
    }));
});
Codificación con espiga
fuente
9
La pregunta es sobre los servicios, que no se instancian en el conjunto de pruebas con una llamada a ningún servicio equivalente como $ controller. En otras palabras, uno no llama a $ service () en un bloque beforeEach, pasando dependencias.
Morris Singer
1

Recientemente lancé ngImprovedTesting que debería facilitar las pruebas simuladas en AngularJS.

Para probar 'myService' (del módulo "myApp") con sus dependencias fooService y barService simuladas, puede hacer lo siguiente en su prueba Jasmine:

beforeEach(ModuleBuilder
    .forModule('myApp')
    .serviceWithMocksFor('myService', 'fooService', 'barService')
    .build());

Para obtener más información sobre ngImprovedTesting, consulte su entrada de blog introductoria: http://blog.jdriven.com/2014/07/ng-improved-testing-mock-testing-for-angularjs-made-easy/

Emil van Galen
fuente
1
¿Por qué se votó en contra? No entiendo el valor de votar en contra sin un comentario.
Jacob Brewer
0

Sé que esta es una pregunta antigua, pero hay otra manera más fácil, puede crear un simulacro y deshabilitar el original inyectado en una función, se puede hacer usando spyOn en todos los métodos. ver código a continuación.

var mockInjectedProvider;

    beforeEach(function () {
        module('myModule');
    });

    beforeEach(inject(function (_injected_) { 
      mockInjectedProvider  = mock(_injected_);
    });

    beforeEach(inject(function (_base_) {
        baseProvider = _base_;
    }));

    it("injectedProvider should be mocked", function () {
    mockInjectedProvider.myFunc.andReturn('testvalue');    
    var resultFromMockedProvider = baseProvider.executeMyFuncFromInjected();
        expect(resultFromMockedProvider).toEqual('testvalue');
    }); 

    //mock all service methods
    function mock(angularServiceToMock) {

     for (var i = 0; i < Object.getOwnPropertyNames(angularServiceToMock).length; i++) {
      spyOn(angularServiceToMock,Object.getOwnPropertyNames(angularServiceToMock)[i]);
     }
                return angularServiceToMock;
    }
Gal Morad
fuente