如何在 AngularJS Jasmine 单元测试中模拟返回承诺的服务?

我有使用 myOtherServicemyService,它可以进行远程呼叫,返回承诺:

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


return {
makeRemoteCall: makeRemoteCall
};
}
])

为了对 myService进行单元测试,我需要模拟 myOtherService,以便它的 makeRemoteCallReturningPromise方法返回一个承诺。我是这么做的:

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

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

做这件事的正确方法是什么?

155444 次浏览

我不知道为什么你这样做不工作,但我通常做的 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');
});
}));

还要记住,您需要为要调用的 then函数进行 $digest调用。参见 $q 文件测试部分。

——编辑——

在仔细研究了您正在做的事情之后,我想我看到了您代码中的问题。在 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)

您可以使用类似 sinon 的存根库来嘲笑您的服务。然后您可以返回 $q.when ()作为您的承诺。如果范围对象的值来自承诺结果,则需要调用范围。$根。$消化()。

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




});

说真的。.通过依赖注入来模拟服务而不是模块,这是错误的。另外,在 before 中调用注入是一种反模式,因为它使得在每个测试的基础上进行模拟变得困难。

我会这么做。

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

现在,当您注入服务时,它将有一个适当的模拟方法供使用。

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:

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

控制器:

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

测试

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

可以用更简洁的形式来写:

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