Comment se moquer d'un service qui renvoie une promesse dans le test unitaire AngularJS Jasmine?

152

J'ai myServicecette utilisation myOtherService, qui fait un appel à distance, promesse de retour:

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

      return {
        makeRemoteCall: makeRemoteCall
      };      
    }
  ])

Pour faire un test unitaire, myServiceje dois me moquer myOtherService, de sorte que sa makeRemoteCallReturningPromiseméthode renvoie une promesse. Voici comment je 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 $qce que je dois charger en utilisant inject(). De plus, l'injection du simulacre devrait se produire module(), ce qui devrait arriver avant inject(). Cependant, la valeur de la maquette n'est pas mise à jour une fois que je la modifie.

Quelle est la bonne façon de procéder?

Georgii Oleinikov
la source
L'erreur est-elle vraiment allumée myService.makeRemoteCall()? Si c'est le cas, le problème est de myServicene pas avoir le makeRemoteCall, rien à voir avec votre moqueur myOtherService.
dnc253
L'erreur est sur myService.makeRemoteCall (), car myService.myOtherService est juste un objet vide à ce stade (sa valeur n'a jamais été mise à jour par angular)
Georgii Oleinikov
Vous ajoutez l'objet vide au conteneur ioc, après quoi vous modifiez la référence myOtherServiceMock pour qu'elle pointe vers un nouvel objet que vous espionnez. Ce qui se trouve dans le conteneur ioc ne reflétera pas cela, car la référence est modifiée.
twDuke

Réponses:

175

Je ne sais pas pourquoi la façon dont vous l'avez fait ne fonctionne pas, mais je le fais généralement avec la spyOnfonction. 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');
      });    
  }));

N'oubliez pas également que vous devrez effectuer un $digestappel pour que la thenfonction soit appelée. Voir la section Test de la documentation $ q .

------ÉDITER------

Après avoir regardé de plus près ce que vous faites, je pense voir le problème dans votre code. Dans le beforeEach, vous définissez myOtherServiceMockun tout nouvel objet. Le $providene 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;   
    };
  }
dnc253
la source
1
Et vous m'avez tué hier en ne vous présentant pas dans les résultats. Bel affichage de andCallFake (). Je vous remercie.
Priya Ranjan Singh
Au lieu de cela, andCallFakevous pouvez utiliser andReturnValue(deferred.promise)(ou and.returnValue(deferred.promise)dans Jasmine 2.0+). Vous devez bien sûr définir deferredavant d'appeler spyOn.
Jordan Running
1
Comment appelleriez-vous $digestdans ce cas lorsque vous n'avez pas accès à l'étendue?
Jim Aho
7
@JimAho En règle générale, vous injectez $rootScopeet appelez $digestcela.
dnc253
1
L'utilisation différée dans ce cas n'est pas nécessaire. Vous pouvez simplement utiliser $q.when() codelord.net/2015/09/24/$q-dot-defer-youre-doing-it-wrong
fodma1
69

Nous pouvons également écrire l'implémentation de la promesse de retour par jasmine directement par espion.

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

Pour Jasmine 2:

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

(copié à partir des commentaires, grâce aux ccnokes)

Priya Ranjan Singh
la source
12
Note aux utilisateurs de Jasmine 2.0, .andReturn () a été remplacé par .and.returnValue. L'exemple ci-dessus serait donc: spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));je viens de tuer une demi-heure pour le comprendre.
ccnokes le
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
la source
1
C'est ainsi que je l'ai fait dans le passé. Créer un espion qui renvoie un faux qui imite le «alors»
Darren Corbett
Pouvez-vous donner un exemple du test complet que vous avez. J'ai un problème similaire d'avoir un service qui renvoie une promesse, mais avec dedans aussi un appel qui renvoie une promesse!
Rob Paddock
Salut Rob, je ne sais pas pourquoi vous voudriez vous moquer d'un appel qu'un simulacre fait à un autre service, vous voudrez sûrement le tester lors du test de cette fonction. Si la fonction vous appelle des appels moqueurs, un service obtient des données, puis affecte les données que votre promesse simulée renverrait un faux ensemble de données affecté, du moins c'est comme ça que je le ferais.
Darren Corbett
J'ai commencé dans cette voie et cela fonctionne très bien pour des scénarios simples. J'ai même créé une maquette qui simule le chaînage et fournit des aides "keep" / "break" pour appeler la chaîne gist.github.com/marknadig/c3e8f2d3fff9d22da42b Dans des scénarios plus complexes, cela tombe cependant. Dans mon cas, j'avais un service qui retournait conditionnellement les éléments d'un cache (avec différé) ou faisait une demande. Donc, cela créait sa propre promesse.
Mark Nadig
Cet article ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy décrit l'utilisation du faux "alors" de manière approfondie.
Custodio
8

Vous pouvez utiliser une bibliothèque de stubbing comme sinon pour vous moquer de votre service. Vous pouvez ensuite retourner $ q.when () comme 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);


    });
Mike Lunn
la source
2
c'est la pièce manquante, appelant $rootScope.$digest()pour obtenir la promesse d'être résolue
2

utilisant sinon:

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

Sachant que, httpPromisepeut être:

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

Honnêtement, vous vous trompez en vous appuyant sur inject pour simuler un service au lieu d'un module. De plus, appeler inject dans un beforeEach est un anti-pattern car il rend la moquerie difficile pour chaque test.

Voici comment je procéderais ...

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, il disposera d'une méthode d'utilisation correctement simulée.

Keith Loy
la source
3
L'intérêt d'un avant chacun est qu'il est appelé avant chaque test.Je ne sais pas comment vous écrivez vos tests mais personnellement j'écris plusieurs tests pour une seule fonction, donc j'aurais une base commune qui serait appelée avant chaque test. Vous voudrez peut-être également rechercher la signification comprise de l'anti pattern car elle est associée au génie logiciel.
Darren Corbett le
0

J'ai trouvé cette fonction de service utile et lancinante comme sinon.stub (). Returns ($ 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 tester

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

this.scope.$digest();

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

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