我如何模拟一个在Angularjs Jasmineunit testing中返回承诺的服务?

我有我的服务,使用myOtherService,这使远程调用,返回承诺:

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

要为myService进行unit testing,我需要模拟myOtherService ,使其makeRemoteCallReturningPromise()方法返回一个promise。 这是我如何做到的:

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

正如你从上面看到的,我的模拟的定义取决于$q ,我必须使用inject()加载。 此外,注入模拟应该发生在module() ,它应该在inject()之前。 但是,一旦我改变它,模拟的值不会更新。

什么是正确的方法来做到这一点?

我不知道为什么你这样做是行不通的,但我通常用spyOn函数spyOn 。 像这样的东西:

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

另外请记住,您将需要做一个$digest调用的函数被调用。 请参阅$ q文档的testing部分。

– – – 编辑 – – –

仔细看看你在做什么,我想我在你的代码中看到了问题。 在beforeEach ,您将myOtherServiceMock设置为一个全新的对象。 $provide将永远不会看到这个引用。 你只需要更新现有的参考:

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

我们也可以直接由间谍写茉莉花的回执。

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

对于茉莉花2:

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

(抄评,感谢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(); }); }); 

你可以使用像sinon这样的存根库来嘲笑你的服务。 然后你可以返回$ q.when()作为你的承诺。 如果您的范围对象的值来自promise结果,您将需要调用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); }); 

老实说,你正在靠这个错误的方式来依靠注入来模拟一个服务而不是模块。 另外,在beforeEach中调用注入是一种反模式,因为它使每个testing都很困难。

这是我怎么做这个…

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

现在,当你注入你的服务时,会有一个适当的模拟方法来使用。

sinon

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

已知的是, httpPromise可以是:

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

我发现sinon.stub()返回($ 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 ); 

控制器:

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

并testing

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

代码片段:

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

可以写成更简洁的forms:

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