2013-02-24 157 views
22

我使用茉莉單元測試的angularjs控制器上設置的範圍的變量來調用返回一個承諾對象服務的方法的結果被解析該服務:Angularjs承諾不在單元測試

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

這在我的angularjs應用程序的上下文中工作正常,但不能在茉莉花單元測試中工作。我已經確認「then」回調正在單元測試中執行,但$ scope.myVar promise永遠不會被設置爲回調的返回值。

我的單元測試:

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

另外,如果我控制器更改爲以下的單元測試通過:

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

爲什麼承諾回調結果值不被傳播到$範圍.myVar在單元測試中?看到下面的jsfiddle的全部工作代碼http://jsfiddle.net/s7PGg/5/

回答

21

我猜這個「神祕」的關鍵是AngularJS會自動解決承諾(和渲染結果),如果那些在模板中的插值指令中使用的事實。我的意思是,鑑於此控制器:

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

和模板:

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

AngularJS,在$ HTTP調用完成後,將「看見」一個承諾得到解決,將重新呈現模板與解決的結果。這是在$q documentation隱約提到:

$ Q承諾在角模板引擎,確認其 意味着模板,你可以把連接到一個範圍,因爲 承諾,如果他們所得到的值。

發生這種奇蹟的代碼可以看到here

但是,這個「魔術」只有在有模板(更確切地說$parse服務)時纔會發揮作用。 在您的單元測試中沒有涉及的模板,因此承諾解析不會自動傳播

現在,我必須說,這個自動解析/結果傳播非常方便,但可能會讓人困惑,正如我們從這個問題中所看到的。這就是爲什麼我喜歡明確的傳播解析結果像你一樣:

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

偉大的答案,我似乎已經錯過了在文檔該位。 – robbymurphy 2013-02-24 16:11:26

+0

如果您像我一樣嘲笑後端,結果將是一個包含實際響應內容的屬性「數據」的組合。 – Gepsens 2013-05-26 18:12:42

+3

在Angular 1.2中,承諾不再自動解決(AKA,unwrapped)。 – zhon 2014-10-16 05:11:56

3

@ pkozlowski.opensource回答了爲什麼(THANK YOU!),但不知道如何繞過它在測試中。

我剛到該解決方案是測試HTTP獲取調用的服務,然後在控制器的測試服務方法窺探並返回,而不是承諾的實際值。

假設我們有一個用戶服務,會談到我們的服務器:

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 
    }, 

    // ... 

    }; 
}); 

測試該服務,我們不關心會發生什麼情況返回的值,只有在HTTP調用正確地進行了。

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

現在在我們的控制器測試中,不需要擔心HTTP。我們只想看到用戶服務正在發揮作用。

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

爲了測試這個,我們監視用戶服務。

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

無需解決承諾。希望這可以幫助。

+0

在測試服務的例子中,我們在調用$ httpBackend.flush()之前必須添加以下內容: $ rootScope。$ apply() – blaster 2013-05-15 14:05:19

+2

@blaster ...然後你做錯了什麼。服務應該在控制器和範圍之外進行測試。 – 2013-09-21 16:02:17

8

我有一個類似的問題,並讓我的控制器直接將$ scope.myVar分配給promise。然後,在測試中,我鎖定了另一個承諾,即在承諾解決時聲明預期的承諾價值。我用了一個輔助方法是這樣的:

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

注意,取決於當你調用expectPromisedValue並在承諾由被測代碼解析的順序,你可能需要手動觸發最終消化週期運行以獲得這些then()方法來觸發 - 無需它,無論resolvedValue是否等於expectedValue,您的測試都可以通過。

爲了安全起見,把觸發器在afterEach()調用,所以你不必記住它爲每一個測試:

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