2013-02-24 122 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(); 
}));