¿Cómo me burlo de un servicio que devuelve la promesa en la prueba de unidad AngularJS Jasmine?

152

Tengo myServiceese uso myOtherService, que hace una llamada remota, devolviendo la promesa:

angular.module('app.myService', ['app.myOtherService'])
  .factory('myService', [
    myOtherService,
    function(myOtherService) {
      function makeRemoteCall() {
        return myOtherService.makeRemoteCallReturningPromise();
      }

      return {
        makeRemoteCall: makeRemoteCall
      };      
    }
  ])

Para hacer una prueba unitaria myService, necesito burlarme myOtherService, de modo que su makeRemoteCallReturningPromisemétodo devuelva una promesa. Así es como lo hago:

describe('Testing remote call returning promise', function() {
  var myService;
  var myOtherServiceMock = {};

  beforeEach(module('app.myService'));

  // I have to inject mock when calling module(),
  // and module() should come before any inject()
  beforeEach(module(function ($provide) {
    $provide.value('myOtherService', myOtherServiceMock);
  }));

  // However, in order to properly construct my mock
  // I need $q, which can give me a promise
  beforeEach(inject(function(_myService_, $q){
    myService = _myService_;
    myOtherServiceMock = {
      makeRemoteCallReturningPromise: function() {
        var deferred = $q.defer();

        deferred.resolve('Remote call result');

        return deferred.promise;
      }    
    };
  }

  // Here the value of myOtherServiceMock is not
  // updated, and it is still {}
  it('can do remote call', inject(function() {
    myService.makeRemoteCall() // Error: makeRemoteCall() is not defined on {}
      .then(function() {
        console.log('Success');
      });    
  }));  

Como puede ver en lo anterior, la definición de mi simulacro depende de $qqué debo cargar usando inject(). Además, inyectar el simulacro debería estar sucediendo en module(), lo que debería venir antes inject(). Sin embargo, el valor del simulacro no se actualiza una vez que lo cambio.

¿Cuál es la forma apropiada de hacer esto?

Georgii Oleinikov
fuente
¿Está realmente el error encendido myService.makeRemoteCall()? Si es así, el problema es myServiceno tener makeRemoteCallnada que ver con su burla myOtherService.
dnc253
El error está en myService.makeRemoteCall (), porque myService.myOtherService es solo un objeto vacío en este momento (su valor nunca fue actualizado por angular)
Georgii Oleinikov
Agregue el objeto vacío al contenedor ioc, después de eso cambia la referencia myOtherServiceMock para que apunte a un nuevo objeto que espíe. Lo que hay en el contenedor ioc no reflejará eso, ya que la referencia cambia.
twDuke

Respuestas:

175

No estoy seguro de por qué la forma en que lo hiciste no funciona, pero generalmente lo hago con la spyOnfunción. Algo como esto:

describe('Testing remote call returning promise', function() {
  var myService;

  beforeEach(module('app.myService'));

  beforeEach(inject( function(_myService_, myOtherService, $q){
    myService = _myService_;
    spyOn(myOtherService, "makeRemoteCallReturningPromise").and.callFake(function() {
        var deferred = $q.defer();
        deferred.resolve('Remote call result');
        return deferred.promise;
    });
  }

  it('can do remote call', inject(function() {
    myService.makeRemoteCall()
      .then(function() {
        console.log('Success');
      });    
  }));

También recuerde que usted tendrá que hacer una $digestllamada a la thenfunción a ser llamada. Consulte la sección Pruebas de la documentación de $ q .

------EDITAR------

Después de mirar más de cerca lo que estás haciendo, creo que veo el problema en tu código. En el beforeEach, está configurando myOtherServiceMockun objeto completamente nuevo. El $providenunca verá esta referencia. Solo necesita actualizar la referencia existente:

beforeEach(inject( function(_myService_, $q){
    myService = _myService_;
    myOtherServiceMock.makeRemoteCallReturningPromise = function() {
        var deferred = $q.defer();
        deferred.resolve('Remote call result');
        return deferred.promise;   
    };
  }
dnc253
fuente
1
Y me mataste ayer al no aparecer en los resultados. Hermosa pantalla de andCallFake (). Gracias.
Priya Ranjan Singh
En lugar de andCallFakeusted puede usar andReturnValue(deferred.promise)(o and.returnValue(deferred.promise)en Jasmine 2.0+). Necesita definir deferredantes de llamar spyOn, por supuesto.
Jordan Running
1
¿Cómo llamaría $digesten este caso cuando no tiene acceso al alcance?
Jim Aho
77
@JimAho Por lo general, solo inyecta $rootScopey llama $digesta eso.
dnc253
1
El uso diferido en este caso es innecesario. Puede usar $q.when() codelord.net/2015/09/24/$q-dot-defer-youre-doing-it-wrong
fodma1
69

También podemos escribir la implementación de la promesa de devolución de jazmín directamente por espía.

spyOn(myOtherService, "makeRemoteCallReturningPromise").andReturn($q.when({}));

Para Jasmine 2:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));

(copiado de los comentarios, gracias a ccnokes)

Priya Ranjan Singh
fuente
12
Nota para las personas que usan Jasmine 2.0, .andReturn () ha sido reemplazado por .and.returnValue. Entonces, el ejemplo anterior sería: spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));acabo de matar media hora averiguando eso.
ccnokes
13
describe('testing a method() on a service', function () {    

    var mock, service

    function init(){
         return angular.mock.inject(function ($injector,, _serviceUnderTest_) {
                mock = $injector.get('service_that_is_being_mocked');;                    
                service = __serviceUnderTest_;
            });
    }

    beforeEach(module('yourApp'));
    beforeEach(init());

    it('that has a then', function () {
       //arrange                   
        var spy= spyOn(mock, 'actionBeingCalled').and.callFake(function () {
            return {
                then: function (callback) {
                    return callback({'foo' : "bar"});
                }
            };
        });

        //act                
        var result = service.actionUnderTest(); // does cleverness

        //assert 
        expect(spy).toHaveBeenCalled();  
    });
});
Darren Corbett
fuente
1
Así es como lo hice en el pasado. Crea un espía que devuelve un falso que imita el "entonces"
Darren Corbett
¿Puede proporcionar un ejemplo de la prueba completa que tiene? ¡Tengo un problema similar de tener un servicio que devuelve una promesa, pero también incluye una llamada que devuelve una promesa!
Rob Paddock
Hola Rob, no estoy seguro de por qué querrías burlarte de una llamada que hace un simulacro a otro servicio, seguramente querrás probar eso cuando pruebes esa función. Si la función le llama burlas, un servicio obtiene datos y luego afecta los datos que su promesa burlada devolvería un conjunto de datos afectados falsos, al menos así es como lo haría.
Darren Corbett
Empecé por este camino y funciona muy bien para escenarios simples. Incluso creé un simulacro que simula el encadenamiento y proporciona ayudantes para "mantener" / "romper" para invocar la cadena gist.github.com/marknadig/c3e8f2d3fff9d22da42b Sin embargo, en escenarios más complejos, esto se cae. En mi caso, tenía un servicio que condicionalmente devolvería elementos de un caché (w / diferido) o haría una solicitud. Entonces, estaba creando su propia promesa.
Mark Nadig
Esta publicación ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy describe el uso del falso "entonces" a fondo.
Custodio
8

Puede usar una biblioteca de stubbing como sinon para burlarse de su servicio. Luego puede devolver $ q.when () como su promesa. Si el valor de su objeto de alcance proviene del resultado de la promesa, deberá llamar a scope. $ Root. $ Digest ().

var scope, controller, datacontextMock, customer;
  beforeEach(function () {
        module('app');
        inject(function ($rootScope, $controller,common, datacontext) {
            scope = $rootScope.$new();
            var $q = common.$q;
            datacontextMock = sinon.stub(datacontext);
            customer = {id:1};
           datacontextMock.customer.returns($q.when(customer));

            controller = $controller('Index', { $scope: scope });

        })
    });


    it('customer id to be 1.', function () {


            scope.$root.$digest();
            expect(controller.customer.id).toBe(1);


    });
Mike Lunn
fuente
2
Esta es la pieza que falta, pidiendo $rootScope.$digest()que se resuelva la promesa
2

utilizando sinon:

const mockAction = sinon.stub(MyService.prototype,'actionBeingCalled')
                     .returns(httpPromise(200));

Conocido eso, httpPromisepuede ser:

const httpPromise = (code) => new Promise((resolve, reject) =>
  (code >= 200 && code <= 299) ? resolve({ code }) : reject({ code, error:true })
);
Abdennour TOUMI
fuente
0

Honestamente ... vas a hacer esto de manera incorrecta confiando en inyectar para burlarse de un servicio en lugar de un módulo. Además, llamar a inyectar en beforeEach es un antipatrón, ya que dificulta la burla por prueba.

Así es como haría esto ...

module(function ($provide) {
  // By using a decorator we can access $q and stub our method with a promise.
  $provide.decorator('myOtherService', function ($delegate, $q) {

    $delegate.makeRemoteCallReturningPromise = function () {
      var dfd = $q.defer();
      dfd.resolve('some value');
      return dfd.promise;
    };
  });
});

Ahora, cuando inyecte su servicio, tendrá un método de uso burlado correctamente.

Keith Loy
fuente
3
El punto principal de un antes de cada uno es que se llama antes de cada prueba. No sé cómo escribir sus pruebas, pero personalmente escribo varias pruebas para una sola función, por lo tanto, tendría una base común configurada que se llamaría antes cada prueba También es posible que desee buscar el significado entendido de anti patrón, ya que se asocia a la ingeniería de software.
Darren Corbett
0

Encontré esa útil función de servicio de apuñalamiento como sinon.stub (). Return ($ q.when ({})):

this.myService = {
   myFunction: sinon.stub().returns( $q.when( {} ) )
};

this.scope = $rootScope.$new();
this.angularStubs = {
    myService: this.myService,
    $scope: this.scope
};
this.ctrl = $controller( require( 'app/bla/bla.controller' ), this.angularStubs );

controlador:

this.someMethod = function(someObj) {
   myService.myFunction( someObj ).then( function() {
        someObj.loaded = 'bla-bla';
   }, function() {
        // failure
   } );   
};

y prueba

const obj = {
    field: 'value'
};
this.ctrl.someMethod( obj );

this.scope.$digest();

expect( this.myService.myFunction ).toHaveBeenCalled();
expect( obj.loaded ).toEqual( 'bla-bla' );
Dmitri Algazin
fuente
-1

El fragmento de código:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.callFake(function() {
    var deferred = $q.defer();
    deferred.resolve('Remote call result');
    return deferred.promise;
});

Se puede escribir en una forma más concisa:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue(function() {
    return $q.resolve('Remote call result');
});
trunikov
fuente