2013-02-24 12 views
22
aufgelöst

I Jasmin zu Unit-Test ein AngularJS Controller verwende, die eine Variable auf den Umfang des Ergebnisses setzt zur Herstellung eines Service-Methode aufrufen, die ein Versprechen Objekt zurückgibt:AngularJS versprechen, nicht in Unit-Test ist

var MyController = function($scope, service) { 
    $scope.myVar = service.getStuff(); 
} 

innen der Service:

function getStuff() { 
    return $http.get('api/stuff').then(function (httpResult) { 
     return httpResult.data; 
    }); 
} 

Dies funktioniert im Rahmen meiner AngularJS Anwendung in Ordnung, aber in dem Jasmin Unit-Test nicht funktioniert. Ich habe bestätigt, dass der "then" Callback im Komponententest ausgeführt wird, aber das $ scope.myVar Versprechen wird niemals auf den Rückgabewert des Callbacks gesetzt.

My Unit-Test:

describe('My Controller', function() { 
    var scope; 
    var serviceMock; 
    var controller; 
    var httpBackend; 

    beforeEach(inject(function ($rootScope, $controller, $httpBackend, $http) { 
    scope = $rootScope.$new(); 
    httpBackend = $httpBackend; 
    serviceMock = { 
     stuffArray: [{ 
     FirstName: "Robby" 
     }], 

     getStuff: function() { 
     return $http.get('api/stuff').then(function (httpResult) { 
      return httpResult.data; 
     }); 
     } 
    }; 
    $httpBackend.whenGET('api/stuff').respond(serviceMock.stuffArray); 
    controller = $controller(MyController, { 
     $scope: scope, 
     service: serviceMock 
    }); 
    })); 

    it('should set myVar to the resolved promise value', 
    function() { 
     httpBackend.flush(); 
     scope.$root.$digest(); 
     expect(scope.myVar[0].FirstName).toEqual("Robby"); 
    }); 
}); 

Auch, wenn ich den Regler auf die im Anschluss an die Einheit Test bestanden ändern:

var MyController = function($scope, service) { 
    service.getStuff().then(function(result) { 
     $scope.myVar = result; 
    }); 
} 

Warum das Ergebnis Versprechen Rückruf Wert nicht mehr als $ Umfang wird propagierten .myVar im Komponententest? Siehe den folgenden jsfiddle für vollen Arbeitscode http://jsfiddle.net/s7PGg/5/

Antwort

21

Ich denke, der Schlüssel zu diesem "Geheimnis" ist die Tatsache, dass AngularJS Versprechungen (und Renderergebnisse) automatisch löst, wenn diese in einer Interpolationsanweisung in einer Vorlage verwendet werden. Was ich meine ist, dass diese Steuerung gegeben:

MyCtrl = function($scope, $http) { 
    $scope.promise = $http.get('myurl', {..}); 
} 

und die Vorlage:

<span>{{promise}}</span> 

AngularJS, auf $ Abschluss http Aufruf, „sehen“, dass ein Versprechen gelöst wurde und wird neu rendern Vorlage mit den aufgelösten Ergebnissen. Dies ist, was vage in den $q documentation erwähnt wird:

$ q Versprechungen der Template-Engine in Winkel erkannt werden, was bedeutet, dass in Vorlagen, die Sie an einen Umfang als angebracht Versprechen behandeln können, wenn sie die resultierenden Werte waren .

Der Code, wo diese Magie passiert, kann here gesehen werden.

ABER, diese "Magie" passiert nur, wenn es eine Vorlage ($parse Service, um genauer zu sein) bei der Wiedergabe ist. In Ihrem Komponententest ist keine Vorlage beteiligt, daher wird die Verheißungsauflösung nicht automatisch weitergegeben.

Jetzt muss ich sagen, dass diese automatische Auflösung/Ergebnisausbreitung sehr bequem ist, aber könnte verwirrend sein, wie wir aus dieser Frage sehen können. Aus diesem Grunde ziehe ich ausdrücklich Auflösung Ergebnisse zu verbreiten, wie Sie tut:

var MyController = function($scope, service) { 
    service.getStuff().then(function(result) { 
     $scope.myVar = result; 
    }); 
} 
+0

große Antwort, schien ich, dass etwas in der Dokumentation verpasst zu haben. – robbymurphy

+0

Wenn Sie das Backend verspotten, wie ich bin, wird das Ergebnis ein Composite mit einer Eigenschaft "Daten" sein, die den eigentlichen Antwortinhalt enthält. – Gepsens

+3

In Angular 1.2 werden Versprechungen nicht mehr automatisch aufgelöst (AKA, unwrapped). – zhon

3

@ pkozlowski.opensource antworteten die, warum (DANKE!), Aber nicht, wie um sie bei der Prüfung zu erhalten.

Die Lösung, die ich gerade gefunden habe, besteht darin, zu testen, dass HTTP im Dienst aufgerufen wird, und dann die Dienstmethoden in den Controller-Tests auszuspionieren und tatsächliche Werte anstelle von Versprechen zurückzugeben.

Angenommen, wir haben einen User-Dienst, der auf unserem Server spricht:

var services = angular.module('app.services', []); 

services.factory('User', function ($q, $http) { 

    function GET(path) { 
    var defer = $q.defer(); 
    $http.get(path).success(function (data) { 
     defer.resolve(data); 
    } 
    return defer.promise; 
    } 

    return { 
    get: function (handle) { 
     return GET('/api/' + handle); // RETURNS A PROMISE 
    }, 

    // ... 

    }; 
}); 

Testing, dass der Service, wir kümmern uns nicht, was mit den zurückgegebenen Werte geschieht, nur, dass die HTTP-Anrufe korrekt vorgenommen wurden.

describe 'User service', -> 
    User = undefined 
    $httpBackend = undefined 

    beforeEach module 'app.services' 

    beforeEach inject ($injector) -> 
    User = $injector.get 'User' 
    $httpBackend = $injector.get '$httpBackend' 

    afterEach -> 
    $httpBackend.verifyNoOutstandingExpectation() 
    $httpBackend.verifyNoOutstandingRequest()   

    it 'should get a user', -> 
    $httpBackend.expectGET('/api/alice').respond { handle: 'alice' } 
    User.get 'alice' 
    $httpBackend.flush()  

Jetzt in unseren Controller-Tests müssen Sie sich keine Sorgen mehr über HTTP machen. Wir möchten nur sehen, dass der Benutzerdienst zum Laufen gebracht wird.

angular.module('app.controllers') 
    .controller('UserCtrl', function ($scope, $routeParams, User) { 
    $scope.user = User.get($routeParams.handle); 
    }); 

Um dies zu testen, spionieren wir den Benutzerdienst aus.

describe 'UserCtrl',() -> 

    User = undefined 
    scope = undefined 
    user = { handle: 'charlie', name: 'Charlie', email: '[email protected]' } 

    beforeEach module 'app.controllers' 

    beforeEach inject ($injector) -> 
    # Spy on the user service 
    User = $injector.get 'User' 
    spyOn(User, 'get').andCallFake -> user 

    # Other service dependencies 
    $controller = $injector.get '$controller' 
    $routeParams = $injector.get '$routeParams' 
    $rootScope = $injector.get '$rootScope' 
    scope = $rootScope.$new(); 

    # Set up the controller 
    $routeParams.handle = user.handle 
    UserCtrl = $controller 'UserCtrl', $scope: scope 

    it 'should get the user by :handle', -> 
    expect(User.get).toHaveBeenCalledWith 'charlie' 
    expect(scope.user.handle).toBe 'charlie'; 

Keine Notwendigkeit, die Versprechen zu lösen. Hoffe das hilft.

+0

In diesem Beispiel, in dem Sie den Dienst testen, mussten wir vor dem Aufruf von $ httpBackend.flush() folgendes hinzufügen: $ rootScope. $ Apply() – blaster

+2

@blaster ... dann machen Sie etwas falsch. Dienste sollten außerhalb des Kontextes von Controllern und Umfang getestet werden. –

8

Ich hatte ein ähnliches Problem und ließ meinen Controller $ scope.myVar direkt dem Versprechen zuweisen. Dann habe ich im Test ein weiteres Versprechen verkettet, das den erwarteten Wert des Versprechens bestätigt, wenn es gelöst wird. Früher habe ich eine Hilfsmethode wie folgt aus:

var expectPromisedValue = function(promise, expectedValue) { 
    promise.then(function(resolvedValue) { 
    expect(resolvedValue).toEqual(expectedValue); 
    }); 
} 

Hinweis, die in Abhängigkeit von der Reihenfolge der beim expectPromisedValue anrufen und wenn das Versprechen von Ihrem Code unter Test behoben ist, müssen Sie möglicherweise manuell eine endgültige auslösen Zyklus verdauen laufen lassen, um diese then() -Methoden auszulösen - ohne sie kann Ihr Test bestehen, unabhängig davon, ob der resolvedValue dem expectedValue entspricht oder nicht.

Um sicher zu sein, in einem afterEach() -Aufruf den Trigger setzen, so dass Sie nicht daran erinnern für jeden Test müssen:

afterEach(inject(function($rootScope) { 
    $rootScope.$apply(); 
})); 
Verwandte Themen