Как я высмеиваю службу, которая возвращает обещание в Angularjs Jasmine unit test?
У меня есть myService, который использует myOtherService, который делает удаленный вызов, возвращая обещание:
angular.module('app.myService', ['app.myOtherService'])
.factory('myService', [myOtherService,
function(myOtherService) {
function makeRemoteCall() {
return myOtherService.makeRemoteCallReturningPromise();
}
return {
makeRemoteCall: makeRemoteCall
};
}
])
Чтобы сделать unit test для 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');
});
}));
Как видно из вышеизложенного, определение моего макета зависит от $q
, которое я должен загрузить с помощью inject()
. Кроме того, инъекция макета должна происходить в module()
, которая должна предшествовать
inject()
. Однако значение mock не обновляется после его изменения.
Каков правильный способ сделать это?
Ответы
Ответ 1
Я не уверен, почему то, как вы это сделали, не работает, но я обычно делаю это с помощью функции 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
для вызываемой функции then
. См. Раздел Тестирование документа $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;
};
}
Ответ 2
Мы также можем написать реализацию жасмина для возвращаемого обещания непосредственно шпионом.
spyOn(myOtherService, "makeRemoteCallReturningPromise").andReturn($q.when({}));
Для жасмина 2:
spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));
(скопировано из комментариев, благодаря ccnokes)
Ответ 3
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();
});
});
Ответ 4
Вы можете использовать библиотеку stubbing, такую как sinon, чтобы издеваться над вашим сервисом. Затем вы можете вернуть $q.when() в качестве своего обещания. Если значение объекта области видимости получается из результата обещания, вам нужно вызвать область действия. $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);
});
Ответ 5
с помощью 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 })
);
Ответ 6
Честно говоря.. вы делаете это неправильно, полагаясь на инъекцию, чтобы издеваться над сервисом, а не с модулем. Кроме того, вызов инжекции в beforeEach является анти-шаблоном, поскольку он делает насмешливое затруднение на каждой тестовой основе.
Вот как я это сделаю...
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;
};
});
});
Теперь, когда вы вводите свою услугу, у него будет правильно издевавшийся метод для использования.
Ответ 7
Я обнаружил, что полезная функция колчана службы 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
} );
};
и тест
const obj = {
field: 'value'
};
this.ctrl.someMethod( obj );
this.scope.$digest();
expect( this.myService.myFunction ).toHaveBeenCalled();
expect( obj.loaded ).toEqual( 'bla-bla' );
Ответ 8
Фрагмент кода:
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');
});