Como faço para zombar de um serviço que seja promissor no teste de unidade do Angularjs Jasmine?

Eu tenho myService que usa myOtherService, que faz uma chamada remota, retornando promise:

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

Para fazer um teste de unidade para myService , preciso zombar de myOtherService , de modo que seu método makeRemoteCallReturningPromise() retorne uma promise. É assim que eu faço:

 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 você pode ver acima, a definição do meu mock depende de $q , que eu tenho que carregar usando inject() . Além disso, injetar o mock deve estar acontecendo no module() , que deve estar chegando antes de inject() . No entanto, o valor para simulação não é atualizado depois de alterá-lo.

Qual é a maneira correta de fazer isso?

Eu não tenho certeza porque o jeito que você fez isso não funciona, mas eu geralmente faço isso com a function spyOn . Algo assim:

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

Lembre-se também que você precisará fazer uma chamada $digest para que a function then seja chamada. Veja a seção Teste da documentação do $ q .

——EDITAR——

Depois de ver mais de perto o que você está fazendo, acho que vejo o problema em seu código. No beforeEach , você está definindo myOtherServiceMock para um object totalmente novo. O $provide nunca verá esta referência. Você só precisa atualizar a referência existente:

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

Também podemos escrever a implementação do jasmine de retornar a promise diretamente pelo espião.

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

Para Jasmine 2:

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

(copiado de comentários, graças a ccnokes)

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

Você pode usar uma biblioteca de arranhões como sinon para zombar de seu serviço. Você pode retornar $ q.when () como sua promise. Se o valor do seu object de escopo vier do resultado da promise, você precisará chamar o escopo. $ 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); }); 

usando sinon :

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

Conhecido que, httpPromise pode ser:

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

Honestamente .. você está indo sobre isso da maneira errada, contando com injetar para zombar de um serviço em vez de módulo. Além disso, chamar injetar em um beforeEach é um antipadrão, pois dificulta o uso de mocking por teste.

Aqui está como eu faria isso …

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

Agora, quando você injetar seu serviço, ele terá um método apropriadamente ridicularizado para uso.

Eu encontrei essa function de serviço útil, stabbing como sinon.stub (). Retorna ($ 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 } ); }; 

e teste

 const obj = { field: 'value' }; this.ctrl.someMethod( obj ); this.scope.$digest(); expect( this.myService.myFunction ).toHaveBeenCalled(); expect( obj.loaded ).toEqual( 'bla-bla' ); 

O trecho de código:

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

Pode ser escrito de uma forma mais concisa:

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