2013-02-24 12 views
22

używam jaśmin do testów jednostkowych jest angularjs kontroler, który ustawia zmienną w zakresie od wyniku wywołanie metody usługi, która zwraca obiekt obietnicyangularjs nie obiecuję być rozwiązany w badanej jednostki

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

wewnątrz usługa:

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

działa to dobrze w kontekście mojego angularjs aplikacji, ale nie działa w badanej jednostki jaśmin. Potwierdziłem, że wywołanie zwrotne "wtedy" jest wykonywane w teście jednostkowym, ale obietnica $ scope.myVar nigdy nie zostaje ustawiona na wartość zwracaną przez wywołanie zwrotne.

Moje testy jednostkowe:

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

Również, jeśli mogę zmienić sterownika do następnego testu jednostkowego przechodzi:

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

Dlaczego zwrotna wartość wynik obietnica nie są propagowane do $ zakresu .myVar w teście jednostkowym? Zobacz następujący jsfiddle dla kodu pełny roboczego http://jsfiddle.net/s7PGg/5/

Odpowiedz

21

Myślę, że kluczem do tej „tajemnicy” jest fakt, że angularjs automatycznie rozwiązać obietnic (i czynią wyników) czy te używane w dyrektywie interpolacji w szablonie. Chodzi mi o to, że biorąc pod uwagę ten kontroler:

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

i szablonu:

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

angularjs po zakończeniu $ połączenia http, będzie „widzieć”, że obietnica została rozwiązana i ponownie uczynić szablon z rozwiązanymi wynikami. To jest to, co ogólnikowo wspomniano w $q documentation:

$ obietnic q są uznanymi przez silnik szablonów w kątowa, która oznacza, że ​​w szablonach można traktować obietnice załączone do zakresu jak gdyby były otrzymane wartości .

Kod gdzie ta magia widać here.

ALE, ta "magia" dzieje się tylko wtedy, gdy istnieje szablon (usługa $parse, aby być bardziej precyzyjnym) podczas gry. W teście jednostki nie ma żadnego szablonu, więc obiecana rozdzielczość nie jest automatycznie propagowana.

Teraz muszę powiedzieć, że ta automatyczna analiza rozdzielczości/wyników jest bardzo wygodna, ale może być myląca, jak widać z tego pytania. Dlatego wolę jawnie propagują wyniki rozdzielczości jak ty:

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

wielką odpowiedź, wydawało mi się, że brakowało trochę w docs. – robbymurphy

+0

Jeśli kpisz z tylnego końca, tak jak ja, wynikiem będzie kompozycja z właściwością "dane" zawierającą rzeczywistą treść odpowiedzi. – Gepsens

+3

W Angular 1.2 obietnice nie są już automatycznie rozwiązywane (AKA, unwrapped). – zhon

3

@ pkozlowski.opensource odpowiedział dlaczego nie (dziękuję!), Ale jak się wokół niego w testowaniu.

Rozwiązaniem, które właśnie dotarłem, jest przetestowanie, że HTTP jest wywoływany w usłudze, a następnie śledzenie metod serwisowych w testach kontrolera i zwracanie rzeczywistych wartości zamiast obietnic.

Załóżmy, że mamy Użytkownik Serwisu, który mówi do naszego serwera:

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 że usługa, nie obchodzi mnie, co się dzieje z zwracanych wartości, tylko że połączenia HTTP zostały wykonane prawidłowo.

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()  

Teraz w naszych testach sterownika, nie ma potrzeby martwić się o HTTP. Chcemy tylko zobaczyć, że usługa użytkownika jest uruchamiana.

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

Aby to sprawdzić, że szpiegował na służbie użytkownika.

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

Nie trzeba rozwiązywać obietnic. Mam nadzieję że to pomoże.

+0

W tym przykładzie, gdzie testujesz usługę, musieliśmy dodać następujące wartości przed wywołaniem $ httpBackend.flush(): $ rootScope. $ Apply() – blaster

+2

@blaster ... wtedy robisz coś nie tak. Usługi powinny być testowane poza kontekstem kontrolerów i zakresu. –

8

Miałem podobny problem i zostawiłem kontroler przypisując $ scope.myVar bezpośrednio do obietnicy. Następnie w teście przykleiłem kolejną obietnicę, która potwierdza oczekiwaną wartość obietnicy, gdy zostanie rozwiązana. Użyłem metody pomocnika takiego:

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

Uwaga że w zależności od uporządkowania kiedy zadzwonić expectPromisedValue i kiedy obietnica zostanie rozwiązany przez kodzie badanego, może zaistnieć potrzeba ręcznego wyzwalania ostateczna strawić cyklu uruchomić, aby wywołać metody then() - bez niego test może przejść, niezależnie od tego, czy resolvedValue jest równy , czy też nie.

Aby być bezpiecznym, należy umieścić spust w afterEach() połączenia, więc nie trzeba pamiętać, to dla każdego testu:

afterEach(inject(function($rootScope) { 
    $rootScope.$apply(); 
})); 
Powiązane problemy