web-dev-qa-db-fra.com

Comment puis-je simuler un service qui renvoie une promesse dans le test unitaire AngularJS Jasmine?

J'ai myService qui utilise myOtherService, qui effectue un appel distant en retournant la promesse:

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

      return {
        makeRemoteCall: makeRemoteCall
      };      
    }
  ])

Pour effectuer un test unitaire pour myService, je dois me moquer de myOtherService, de sorte que sa méthode makeRemoteCallReturningPromise renvoie une promesse. Voici comment je le fais:

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

Comme vous pouvez le voir ci-dessus, la définition de ma maquette dépend de $q, que je dois charger avec inject(). En outre, l'injection de la maquette doit être effectuée dans module(), qui doit précéder inject(). Cependant, la valeur de la maquette n'est pas mise à jour une fois que je l'ai modifiée.

Quelle est la bonne façon de faire cela?

148

Je ne sais pas pourquoi votre méthode ne fonctionne pas, mais je le fais habituellement avec la fonction spyOn. Quelque chose comme ça:

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

Rappelez-vous également que vous devrez appeler $digest pour que la fonction then soit appelée. Voir la section Testing de la documentation $ q .

------ EDIT ------

Après avoir examiné de plus près ce que vous faites, je pense que je vois le problème dans votre code. Dans le beforeEach, vous définissez myOtherServiceMock sur un nouvel objet. Le $provide ne verra jamais cette référence. Il vous suffit de mettre à jour la référence existante:

beforeEach(inject( function(_myService_, $q){
    myService = _myService_;
    myOtherServiceMock.makeRemoteCallReturningPromise = function() {
        var deferred = $q.defer();
        deferred.resolve('Remote call result');
        return deferred.promise;   
    };
  }
174
dnc253

Nous pouvons également écrire l'implémentation de jasmine de "return promise" directement par espion.

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

Pour Jasmine 2:

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

(copié des commentaires, merci à ccnokes)

69
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();  
    });
});
12
Darren Corbett

Vous pouvez utiliser une librairie stubbing telle que sinon pour se moquer de votre service. Vous pouvez ensuite retourner $ q.when () comme votre promesse. Si la valeur de votre objet scope provient du résultat de la promesse, vous devrez appeler 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);


    });
8
Mike Lunn

en utilisant sinon:

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

Sait que, httpPromise peut être:

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

Honnêtement ... vous vous y prenez de la mauvaise façon en vous fiant à injecter pour simuler un service au lieu d'un module. Également, appeler inject dans beforeEach est un anti-modèle, car il est difficile de se moquer par test.

Voici comment je le ferais ...

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

Désormais, lorsque vous injecterez votre service, la méthode d’utilisation sera correctement simulée.

0
Keith Loy

J'ai trouvé cette fonction de service poignante utile comme 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 );

manette:

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

et test

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

this.scope.$digest();

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

L'extrait de code:

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

Peut être écrit sous une forme plus concise:

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