Як я знущаюся над службою, яка повертає обіцянку в тесті 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.