Wie verspotte ich einen Service, der im AngularJS Jasmine-Unit-Test vielversprechend ist?


152

Ich habe myServicediese Verwendungen myOtherService, die einen Fernanruf tätigen und ein Versprechen zurückgeben:

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

      return {
        makeRemoteCall: makeRemoteCall
      };      
    }
  ])

Um einen Unit-Test für zu machen, muss myServiceich mich verspotten myOtherService, so dass seine makeRemoteCallReturningPromiseMethode ein Versprechen zurückgibt. So mache ich es:

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

Wie Sie oben sehen können, hängt die Definition meines Mocks davon ab $q, mit welcher ich laden muss inject(). Darüber hinaus sollte das Injizieren des Mocks stattfinden module(), was vorher kommen sollte inject(). Der Wert für das Modell wird jedoch nicht aktualisiert, sobald ich es ändere.

Was ist der richtige Weg, um dies zu tun?


Ist der Fehler wirklich eingeschaltet myService.makeRemoteCall()? Wenn ja, liegt das Problem darin, myServicedass Sie makeRemoteCallnichts haben und nichts mit Ihrem Verspotteten zu tun haben myOtherService.
dnc253

Der Fehler ist auf myService.makeRemoteCall (), weil myService.myOtherService zu diesem Zeitpunkt nur ein leeres Objekt ist (sein Wert wurde nie durch Winkel aktualisiert)
Georgii Oleinikov

Sie fügen das leere Objekt dem ioc-Container hinzu und ändern anschließend die Referenz myOtherServiceMock, um auf ein neues Objekt zu verweisen, das Sie ausspionieren. Was sich im ioc-Container befindet, wird dies nicht widerspiegeln, da die Referenz geändert wird.
TwDuke

Antworten:


175

Ich bin mir nicht sicher, warum die Art und Weise, wie Sie es getan haben, nicht funktioniert, aber ich mache es normalerweise mit der spyOnFunktion. Etwas wie das:

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

Denken Sie auch daran, dass Sie einen $digestAufruf tätigen müssen, damit die thenFunktion aufgerufen werden kann. Siehe den Abschnitt Testen in der $ q-Dokumentation .

------BEARBEITEN------

Nachdem ich mir genauer angesehen habe, was Sie tun, sehe ich das Problem in Ihrem Code. In der beforeEachEinstellung setzen Sie myOtherServiceMockauf ein ganz neues Objekt. Der $providewird diese Referenz nie sehen. Sie müssen nur die vorhandene Referenz aktualisieren:

beforeEach(inject( function(_myService_, $q){
    myService = _myService_;
    myOtherServiceMock.makeRemoteCallReturningPromise = function() {
        var deferred = $q.defer();
        deferred.resolve('Remote call result');
        return deferred.promise;   
    };
  }

1
Und du hast mich gestern getötet, weil du nicht in den Ergebnissen aufgetaucht bist. Schöne Anzeige von andCallFake (). Danke dir.
Priya Ranjan Singh

Anstelle von können andCallFakeSie verwenden andReturnValue(deferred.promise)(oder and.returnValue(deferred.promise)in Jasmine 2.0+). Sie müssen natürlich definieren, deferredbevor Sie anrufen spyOn.
Jordan läuft

1
Wie würden Sie $digestin diesem Fall anrufen , wenn Sie keinen Zugriff auf den Bereich haben?
Jim Aho

7
@ JimAho Normalerweise spritzt man einfach $rootScopeund ruft das $digestauf.
dnc253

1
Die Verwendung von verzögert ist in diesem Fall nicht erforderlich. Sie können einfach $q.when() codelord.net/2015/09/24/$q-dot-defer-youre-doing-it-wrong
fodma1

69

Wir können auch Jasmins Umsetzung der Rückgabe von Versprechen direkt durch Spion schreiben.

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

Für Jasmin 2:

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

(aus Kommentaren kopiert, dank ccnokes)


12
Hinweis für Benutzer von Jasmine 2.0: .andReturn () wurde durch .and.returnValue ersetzt. Das obige Beispiel wäre also: spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when({}));Ich habe gerade eine halbe Stunde getötet, um das herauszufinden.
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
So habe ich es in der Vergangenheit gemacht. Erstellen Sie einen Spion, der eine Fälschung zurückgibt, die das "Dann" nachahmt
Darren Corbett

Können Sie ein Beispiel für den vollständigen Test geben, den Sie haben? Ich habe ein ähnliches Problem mit einem Dienst, der ein Versprechen zurückgibt, aber mit ihm auch einen Anruf tätigt, der ein Versprechen zurückgibt!
Rob Paddock

Hallo Rob, ich bin mir nicht sicher, warum Sie einen Anruf verspotten möchten, den ein Verspottung an einen anderen Dienst tätigt. Sicherlich möchten Sie dies testen, wenn Sie diese Funktion testen. Wenn die Funktionsaufrufe, die Sie verspotten, Anrufe eines Dienstes abrufen, wirkt sich dies auf die Daten aus, die Ihr verspottetes Versprechen einen gefälschten betroffenen Datensatz zurückgeben würde, zumindest würde ich das so machen.
Darren Corbett

Ich habe diesen Weg eingeschlagen und er funktioniert hervorragend für einfache Szenarien. Ich habe sogar ein Modell erstellt, das die Verkettung simuliert und "keep" / "break" -Hilfen zum Aufrufen der Kette gist.github.com/marknadig/c3e8f2d3fff9d22da42b bereitstellt. In komplexeren Szenarien fällt dies jedoch aus. In meinem Fall hatte ich einen Service, der bedingt Elemente aus einem Cache zurückgab (mit Zurückstellung) oder eine Anfrage stellte. Es schuf also sein eigenes Versprechen.
Mark Nadig

Dieser Beitrag ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy beschreibt die Verwendung von Fake "dann" ausführlich.
Custodio

8

Sie können eine Stubbing-Bibliothek wie sinon verwenden, um Ihren Dienst zu verspotten. Sie können dann $ q.when () als Ihr Versprechen zurückgeben. Wenn der Wert Ihres Scope-Objekts aus dem Versprechungsergebnis stammt, müssen Sie scope. $ Root. $ Digest () aufrufen.

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
Dies ist das fehlende Stück, das anruft $rootScope.$digest(), um das Versprechen zu lösen

2

mit sinon:

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

Bekannt httpPromisekann das sein:

const httpPromise = (code) => new Promise((resolve, reject) =>
  (code >= 200 && code <= 299) ? resolve({ code }) : reject({ code, error:true })
);

0

Ehrlich gesagt ... Sie gehen dies falsch an, indem Sie sich auf injizieren, um einen Dienst anstelle eines Moduls zu verspotten. Das Aufrufen von inj in einem beforeEach ist ein Anti-Pattern, da es das Verspotten pro Test erschwert.

Hier ist, wie ich das machen würde ...

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

Wenn Sie jetzt Ihren Dienst einspeisen, verfügt er über eine ordnungsgemäß verspottete Verwendungsmethode.


3
Der springende Punkt bei a before each ist, dass es vor jedem Test aufgerufen wird. Ich weiß nicht, wie Sie Ihre Tests schreiben, aber ich persönlich schreibe mehrere Tests für eine einzelne Funktion, daher hätte ich eine gemeinsame Basis eingerichtet, die vorher aufgerufen würde jeder Test. Möglicherweise möchten Sie auch die verstandene Bedeutung von Anti-Pattern nachschlagen, die mit Software-Engineering verbunden ist.
Darren Corbett

0

Ich fand diese nützliche, stechende Dienstfunktion als sinon.stub (). Returns ($ 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 );

Regler:

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

und testen

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

this.scope.$digest();

expect( this.myService.myFunction ).toHaveBeenCalled();
expect( obj.loaded ).toEqual( 'bla-bla' );

-1

Das Code-Snippet:

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

Kann in einer prägnanteren Form geschrieben werden:

spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue(function() {
    return $q.resolve('Remote call result');
});
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.