Як я знущаюся над службою, яка повертає обіцянку в тесті AngularJS Jasmine unit?


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 .

------ EDIT ------

Подивившись ближче на те, що ти робиш, я думаю, що бачу проблему у твоєму коді. У 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;   
    };
  }

1
А ти вчора вбив мене, не показавши результатів. Прекрасний дисплей andCallFake (). Дякую.
Priya Ranjan Singh

Замість цього andCallFakeви можете використовувати andReturnValue(deferred.promise)(або and.returnValue(deferred.promise)в Жасмін 2.0+). Ви повинні визначитися, deferredперш ніж дзвонити spyOn.
Йорданія, що працює

1
Як би ви закликали $digestв цьому випадку, коли у вас немає доступу до сфери застосування?
Джим Ахо

7
@JimAho Зазвичай ви просто вводите ін'єкцію $rootScopeта дзвоніть $digestна це.
dnc253

1
Використання відкладеного в цьому випадку зайве. Ви можете просто скористатися $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({}));я щойно вбив півгодини, розбираючись у цьому.
ccnokes

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
Ось як я це робив у минулому. Створіть шпигуна, який повертає підробку, що імітує "тоді"
Даррен Корбетт

Чи можете ви надати приклад повного тесту, який у вас є. У мене є аналогічна проблема наявності сервісу, який повертає обіцянку, але при цьому він також робить дзвінок, який повертає обіцянку!
Роб Паддок

Привіт Роб, не впевнений, чому ви хочете знущатися над дзвінком, який макет робить іншому сервісу, напевно, ви хочете перевірити це під час тестування цієї функції. Якщо виклики функцій, з яких ви знущаєтесь над дзвінками, сервіс отримує дані, то це впливає на те, що дані, з яких ви знущалися, обіцяють повернути підроблений набір даних, принаймні так я б це зробив.
Даррен Корбетт

Я почав цей шлях, і він чудово підходить для простих сценаріїв. Я навіть створив макет, який імітує ланцюжок і надає помічникам "тримати" / "ламати", щоб викликати ланцюжок gist.github.com/marknadig/c3e8f2d3fff9d22da42b У більш складних сценаріях це все ж падає. У моєму випадку у мене був сервіс, який умовно повертав елементи з кешу (без відстрочки) або робив запит. Отже, це створювало власну обіцянку.
Марк Надіг

У цьому дописі ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy описано використання підробки "потім" наскрізь.
Кастодіо

8

Для знущань над службою можна використовувати бібліотеку із заглушкою, як син. Потім ви можете повернути $ 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

Чесно кажучи .. ви підете про це неправильним шляхом, покладаючись на ін’єкцію, щоб знущатися над службою замість модуля. Крім того, виклик ін'єкцій у програмі 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;
    };
  });
});

Тепер, коли ви вводите свою послугу, вона матиме належну методику для використання.


3
Вся суть a перед кожним полягає в тому, що він викликається перед кожним тестом. Я не знаю, як ви пишете свої тести, але особисто я пишу кілька тестів для однієї функції, тому я мав би спільну базу, яку б називали раніше кожен тест. Крім того, ви можете шукати зрозуміле значення анти-шаблону, оскільки воно пов'язане з інженерією програмного забезпечення.
Даррен Корбетт

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');
});
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.