Come posso prendere in giro un servizio che restituisce risultati promettenti nel test unitario di AngularJS Jasmine?


152

Ho myServiceche usi myOtherService, che effettua una chiamata remota, restituendo promessa:

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

      return {
        makeRemoteCall: makeRemoteCall
      };      
    }
  ])

Per fare un test di unità per il myServiceHo bisogno di prendere in giro myOtherService, in modo tale che il suo makeRemoteCallReturningPromisemetodo restituisce una promessa. Ecco come lo faccio:

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

Come puoi vedere da quanto sopra, la definizione del mio mock dipende da $q, che devo caricare usando inject(). Inoltre, l'iniezione del finto dovrebbe avvenire in module(), che dovrebbe venire prima inject(). Tuttavia, il valore per il mock non viene aggiornato dopo averlo modificato.

Qual è il modo corretto per farlo?


L'errore è davvero acceso myService.makeRemoteCall()? In tal caso, il problema è myServicenon avere il makeRemoteCall, niente a che fare con il tuo deriso myOtherService.
dnc253,

L'errore è su myService.makeRemoteCall (), perché myService.myOtherService è solo un oggetto vuoto a questo punto (il suo valore non è mai stato aggiornato da angolare)
Georgii Oleinikov,

Aggiungete l'oggetto vuoto al contenitore ioc, dopo di che cambiate il riferimento myOtherServiceMock per puntare a un nuovo oggetto su cui spiate. Ciò che è contenuto nel contenitore di ioc non lo rifletterà, poiché il riferimento viene modificato.
twDuke,

Risposte:


175

Non sono sicuro del perché il modo in cui lo hai fatto non funziona, ma di solito lo faccio con la spyOnfunzione. Qualcosa come questo:

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

Ricorda inoltre che dovrai effettuare una $digestchiamata per chiamare la thenfunzione. Vedi la sezione Test della documentazione $ q .

------MODIFICARE------

Dopo aver esaminato più da vicino quello che stai facendo, penso di vedere il problema nel tuo codice. Nel beforeEach, stai impostando myOtherServiceMockun oggetto completamente nuovo. Non $providevedrà mai questo riferimento. Hai solo bisogno di aggiornare il riferimento esistente:

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

1
E mi hai ucciso ieri non presentandoti nei risultati. Bellissimo display di andCallFake (). Grazie.
Priya Ranjan Singh,

Invece di andCallFakete puoi usareandReturnValue(deferred.promise) (o and.returnValue(deferred.promise)in Jasmine 2.0+). È necessario definire deferredprima di chiamare spyOn, ovviamente.
Giordania, in esecuzione il

1
Come chiameresti $digestin questo caso quando non hai accesso all'ambito?
Jim Aho,

7
@JimAho In genere basta iniettare $rootScopee chiamare$digest su quello.
dnc253,

1
L'uso del differito in questo caso non è necessario. Puoi semplicemente usare $q.when() codelord.net/2015/09/24/$q-dot-defer-youre-doing-it-wrong
fodma1

69

Possiamo anche scrivere l'implementazione del gelsomino della promessa di ritorno direttamente dalla spia.

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

Per Jasmine 2:

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

(copiato dai commenti, grazie a ccnokes)


12
Nota per le persone che usano Jasmine 2.0, .andReturn () è stato sostituito da .and.returnValue. Quindi l'esempio sopra sarebbe: spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));ho appena ucciso mezz'ora per capirlo.
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();  
    });
});

1
È così che l'ho fatto in passato. Crea una spia che restituisce un falso che imita il "allora"
Darren Corbett,

Potete fornire un esempio del test completo che avete. Ho un problema simile di avere un servizio che restituisce una promessa, ma con in esso effettua anche una chiamata che restituisce una promessa!
Rob Paddock,

Ciao Rob, non sono sicuro del motivo per cui vorresti deridere una chiamata che un mock fa a un altro servizio sicuramente vorresti verificarlo quando testerai quella funzione. Se le chiamate di funzione che stai deridendo, un servizio ottiene dati, influisce su quei dati che la tua promessa derisa restituirebbe un set di dati interessato falso, almeno è così che lo farei.
Darren Corbett,

Ho iniziato questo percorso e funziona benissimo per scenari semplici. Ho anche creato un finto che simula il concatenamento e fornisce aiutanti "keep" / "break" per invocare la catena gist.github.com/marknadig/c3e8f2d3fff9d22da42b In scenari più complessi, tuttavia, questo cade. Nel mio caso, avevo un servizio che avrebbe restituito condizionalmente gli elementi da una cache (con / differito) o avrebbe effettuato una richiesta. Quindi, stava creando la propria promessa.
Mark Nadig,

Questo post ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy descrive l'utilizzo del falso "then" a fondo.
Custodio,

8

Puoi usare una biblioteca di stub come Sinon per deridere il tuo servizio. È quindi possibile restituire $ q.when () come promessa. Se il valore dell'oggetto scope deriva dal risultato della promessa, dovrai chiamare 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);


    });

2
questo è il pezzo mancante, che chiama $rootScope.$digest()per ottenere la promessa da risolvere

2

utilizzando sinon:

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

Noto che, httpPromisepuò essere:

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

0

Onestamente ... stai andando in questo modo nel modo sbagliato facendo affidamento sull'iniezione per deridere un servizio invece del modulo. Inoltre, chiamare inject in beforeEach è un anti-pattern in quanto rende difficile il deridere in base al test.

Ecco come lo farei ...

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

Ora, quando inietterai il tuo servizio, avrà un metodo deriso per l'uso.


3
Il punto principale di un prima di ciascuno è che viene chiamato prima di ogni test Non so come si scrivono i test ma personalmente scrivo più test per una singola funzione, quindi avrei una configurazione di base comune che sarebbe chiamata prima ogni test. Inoltre, potresti voler cercare il significato compreso di anti pattern in quanto associato all'ingegneria del software.
Darren Corbett,

0

Ho scoperto che il servizio utile e accoltellato funziona come 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 );

Controller:

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

e prova

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

this.scope.$digest();

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

-1

Lo snippet di codice:

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

Può essere scritto in una forma più concisa:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue(function() {
    return $q.resolve('Remote call result');
});
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.