Как мне издеваться над сервисом, который возвращает обещание в модульном тесте AngularJS Jasmine?

152

У меня есть myServiceто , что использует myOtherService, что делает удаленный вызов, возвращая обещание:

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

Как видно из вышесказанного, определение моего макета зависит от того $q, что я должен загрузить, используя inject(). Кроме того, инъекция должна происходить в том месте module(), которое должно быть раньше inject(). Однако значение для макета не обновляется, как только я его изменяю.

Как правильно это сделать?

Георгий Олейников
источник
Ошибка действительно включена myService.makeRemoteCall()? Если так, то проблема в myServiceтом makeRemoteCall, что вы не имеете ничего общего с вашим издевательством myOtherService.
dnc253
Ошибка в myService.makeRemoteCall (), потому что myService.myOtherService на данный момент является просто пустым объектом (его значение никогда не обновлялось по угловым значениям)
Георгий Олейников
Вы добавляете пустой объект в контейнер ioc, после чего изменяете ссылку myOtherServiceMock, чтобы она указывала на новый объект, за которым вы следите. То, что находится в контейнере ioc, не будет отражать это, поскольку ссылка изменяется.
TwDuke

Ответы:

175

Я не уверен, почему то, как ты это сделал, не работает, но я обычно делаю это с помощью 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;   
    };
  }
dnc253
источник
1
И ты убил меня вчера, не показав результаты. Красивое отображение andCallFake (). Спасибо.
Прия Ранджан Сингх
Вместо andCallFakeтебя можно использовать andReturnValue(deferred.promise)(или and.returnValue(deferred.promise)в Жасмин 2.0+). Вы должны определить, deferredпрежде чем позвонить spyOn, конечно.
Джордан, бегущий
1
Как бы вы позвонили $digestв этом случае, когда у вас нет доступа к области?
Джим Ахо
7
@JimAho Как правило, вы просто вводите $rootScopeи вызываете $digestэто.
dnc253
1
Использование deferred в этом случае не нужно. Вы можете просто использовать $q.when() codelord.net/2015/09/24/$q-dot-defer-youre-doing-it-wrong
fodma1
69

Мы также можем написать реализацию возвращаемого обещания Жасмин непосредственно шпионом.

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

Для Жасмин 2:

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

(скопировано из комментариев, спасибо ccnokes)

Прия Ранджан Сингх
источник
12
Примечание для людей, использующих Jasmine 2.0, .andReturn () был заменен на .and.returnValue. Таким образом, приведенный выше пример будет таким: spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));я просто убил полчаса, чтобы понять это.
cnnokes
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
Вот как я это делал в прошлом. Создайте шпиона, который возвращает фальшивку, которая имитирует «тогда»
Даррен Корбетт
Можете ли вы привести пример полного теста, который у вас есть. У меня похожая проблема, связанная с наличием службы, которая возвращает обещание, но при этом также выполняет вызов, который возвращает обещание!
Роб Паддок
Привет, Роб, не уверен, почему ты хочешь смоделировать вызов, который издевается над другим сервисом, и ты наверняка захочешь проверить это при тестировании этой функции. Если вызовы, которые вы выполняете, - это ложные вызовы, служба получает данные, а затем влияет на эти данные, то ваше поддельное обещание вернет ложный набор данных, на который влияют, по крайней мере, так я поступил бы.
Даррен Корбетт
Я начал этот путь, и он отлично работает для простых сценариев. Я даже создал макет, который имитирует цепочку и предоставляет помощники "keep" / "break" для вызова цепочки gist.github.com/marknadig/c3e8f2d3fff9d22da42b Однако в более сложных сценариях это не так. В моем случае у меня был сервис, который мог бы условно возвращать элементы из кэша (с отсрочкой) или делать запрос. Итак, это создавало свое собственное обещание.
Марк Надиг
В этом посте ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy подробно описывается использование фальшивых «тогда».
Кастодио
8

Вы можете использовать библиотеку-заглушку, такую ​​как 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);


    });
Майк Ланн
источник
2
это недостающий фрагмент, призывающий $rootScope.$digest()получить обещание, которое будет решено
2

используя 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 })
);
Абденнур ТУМИ
источник
0

Честно говоря ... вы делаете это неправильно, полагаясь на инъекцию, чтобы смоделировать сервис вместо модуля. Кроме того, вызов inject в beforeEach является анти-паттерном, поскольку он затрудняет mocking для каждого теста.

Вот как бы я это сделал ...

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

Теперь, когда вы вводите свой сервис, у него будет правильно смоделированный метод для использования.

Кит Лой
источник
3
Весь смысл перед каждым состоит в том, что он вызывается перед каждым тестом. Я не знаю, как вы пишете свои тесты, но лично я пишу несколько тестов для одной функции, поэтому у меня была бы общая базовая настройка, которая была бы вызвана раньше. каждый тест. Также вы можете захотеть взглянуть на понятное значение анти-паттерна, связанного с разработкой программного обеспечения.
Даррен Корбетт
0

Я нашел эту полезную сервисную функцию с режущим заголовком как 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' );
Дмитрий Алгазин
источник
-1

Фрагмент кода:

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');
});
trunikov
источник