Jak zakpić usługę, która zwraca obietnicę w teście jednostkowym AngularJS Jasmine?

152

Mam myServiceto zastosowania myOtherService, które wykonuje zdalne połączenie, zwracając obietnicę:

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

      return {
        makeRemoteCall: makeRemoteCall
      };      
    }
  ])

Aby wykonać test jednostkowy myService, muszę mock myOtherService, tak aby jego makeRemoteCallReturningPromisemetoda zwracała obietnicę. Oto jak to robię:

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

Jak widać z powyższego, definicja mojej makiety zależy od tego $q, którą mam załadować za pomocą inject(). Co więcej, wstrzyknięcie makiety powinno mieć miejsce module(), co powinno nastąpić wcześniej inject(). Jednak wartość makiety nie jest aktualizowana po jej zmianie.

Jaki jest właściwy sposób, aby to zrobić?

Georgii Oleinikov
źródło
Czy błąd naprawdę się pojawia myService.makeRemoteCall()? Jeśli tak, problem polega na myServicetym makeRemoteCall, że nie masz nic wspólnego z wyśmiewanym myOtherService.
dnc253
Błąd występuje w myService.makeRemoteCall (), ponieważ myService.myOtherService jest w tym momencie po prostu pustym obiektem (jego wartość nigdy nie była aktualizowana przez
angular
Dodajesz pusty obiekt do kontenera ioc, a następnie zmieniasz odwołanie myOtherServiceMock, aby wskazywało na nowy obiekt, który śledzisz. To, co znajduje się w pojemniku ioc, nie będzie tego odzwierciedlać, ponieważ odniesienie zostanie zmienione.
twDuke

Odpowiedzi:

175

Nie jestem pewien, dlaczego sposób, w jaki to zrobiłeś, nie działa, ale zwykle robię to z spyOnfunkcją. Coś takiego:

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

Pamiętaj również, że będziesz musiał wykonać $digestwywołanie thenfunkcji, która ma zostać wywołana. Zobacz sekcję Testowanie w dokumentacji $ q .

------EDYTOWAĆ------

Po bliższym przyjrzeniu się temu, co robisz, wydaje mi się, że widzę problem w Twoim kodzie. W programie beforeEachustawiasz myOtherServiceMocksię na zupełnie nowy obiekt. $provideNigdy nie zobaczy tego odniesienia. Wystarczy zaktualizować istniejące odniesienie:

beforeEach(inject( function(_myService_, $q){
    myService = _myService_;
    myOtherServiceMock.makeRemoteCallReturningPromise = function() {
        var deferred = $q.defer();
        deferred.resolve('Remote call result');
        return deferred.promise;   
    };
  }
dnc253
źródło
1
I zabiłeś mnie wczoraj, nie pokazując się w wynikach. Piękny wyświetlacz andCallFake (). Dziękuję Ci.
Priya Ranjan Singh
Zamiast tego andCallFakemożesz użyć andReturnValue(deferred.promise)(lub and.returnValue(deferred.promise)w Jasmine 2.0+). Oczywiście musisz to określić, deferredzanim zadzwonisz spyOn.
Jordan Bieganie
1
Jak nazwałbyś to $digestw tym przypadku, gdy nie masz dostępu do zakresu?
Jim Aho
7
@JimAho Zazwyczaj po prostu wstrzykujesz $rootScopei wywołujesz $digestto.
dnc253
1
Używanie odroczonego w tym przypadku nie jest konieczne. Możesz po prostu użyć $q.when() codelord.net/2015/09/24/$q-dot-defer-youre-doing-it-wrong
fodma1
69

Możemy również napisać wykonanie jaśminowej obietnicy zwrotu bezpośrednio przez szpiega.

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

Dla Jasmine 2:

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

(skopiowane z komentarzy dzięki ccnokes)

Priya Ranjan Singh
źródło
12
Uwaga dla osób używających Jasmine 2.0, .andReturn () została zastąpiona przez .and.returnValue. Więc powyższy przykład byłby taki: spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));właśnie zabiłem pół godziny, zastanawiając się nad tym.
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();  
    });
});
Darren Corbett
źródło
1
Tak robiłem to w przeszłości. Tworzenie szpiega, który zwraca fałszywe że naśladuje „potem”
Darren Corbett
Czy możesz podać przykład pełnego testu, który masz. Mam podobny problem z usługą, która zwraca obietnicę, ale w niej również wykonuje połączenie, które zwraca obietnicę!
Rob Paddock
Cześć Rob, nie jestem pewien, dlaczego miałbyś kpić z połączenia, które makieta wykonuje do innej usługi, z pewnością chciałbyś to przetestować podczas testowania tej funkcji. Jeśli funkcja, z której kpisz, wywołuje usługę, która pobiera dane, wpływa to na dane, które twoja fałszywa obietnica zwróci fałszywy zestaw danych, a przynajmniej tak bym to zrobił.
Darren Corbett
Zacząłem od tej ścieżki i sprawdza się świetnie w prostych scenariuszach. Stworzyłem nawet makietę, która symuluje tworzenie łańcuchów i dostarcza pomocników „keep” / „break” do wywoływania łańcucha gist.github.com/marknadig/c3e8f2d3fff9d22da42b W bardziej złożonych scenariuszach to jednak spada. W moim przypadku miałem usługę, która warunkowo zwracała elementy z pamięci podręcznej (bez odroczenia) lub wysyłała żądanie. Więc tworzył swoją własną obietnicę.
Mark Nadig
W tym poście ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy szczegółowo opisano użycie fałszywego „then”.
Custodio
8

Możesz użyć biblioteki do krojenia, takiej jak sinon, aby kpić z twojej usługi. Następnie możesz zwrócić $ q.when () jako swoją obietnicę. Jeśli wartość obiektu zakresu pochodzi z wyniku obietnicy, będziesz musiał wywołać zakres. $ 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);


    });
Mike Lunn
źródło
2
to jest brakujący element, wzywający $rootScope.$digest()do rozwiązania obietnicy
2

używając sinon:

const mockAction = sinon.stub(MyService.prototype,'actionBeingCalled')
                     .returns(httpPromise(200));

Wiadomo, że httpPromisemoże to być:

const httpPromise = (code) => new Promise((resolve, reject) =>
  (code >= 200 && code <= 299) ? resolve({ code }) : reject({ code, error:true })
);
Abdennour TOUMI
źródło
0

Szczerze ... postępujesz w ten sposób w niewłaściwy sposób, polegając na wstrzyknięciu do fałszywej usługi zamiast modułu. Ponadto wywołanie funkcji inject w beforeEach jest anty-wzorcem, ponieważ utrudnia kpienie na podstawie testu.

Oto jak bym to zrobił ...

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

Teraz, gdy wstrzykniesz swoją usługę, będzie ona miała odpowiednio wyszydzoną metodę użycia.

Keith Loy
źródło
3
Cały sens przed każdym polega na tym, że jest wywoływany przed każdym testem Nie wiem, jak piszesz swoje testy, ale osobiście piszę wiele testów dla jednej funkcji, dlatego miałbym wspólną bazę, która byłaby wywoływana wcześniej każdy test. Możesz również zechcieć przyjrzeć się zrozumiałemu znaczeniu anty-wzorca, ponieważ wiąże się on z inżynierią oprogramowania.
Darren Corbett
0

Znalazłem tę przydatną, przeszywającą funkcję usługi jako sinon.stub (). Zwraca ($ 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 );

kontroler:

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

i przetestuj

const obj = {
    field: 'value'
};
this.ctrl.someMethod( obj );

this.scope.$digest();

expect( this.myService.myFunction ).toHaveBeenCalled();
expect( obj.loaded ).toEqual( 'bla-bla' );
Dmitri Algazin
źródło
-1

Fragment kodu:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.callFake(function() {
    var deferred = $q.defer();
    deferred.resolve('Remote call result');
    return deferred.promise;
});

Można zapisać w bardziej zwięzłej formie:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue(function() {
    return $q.resolve('Remote call result');
});
trunikov
źródło