我正在使用jasmine对angularjs控制器进行单元测试,该控制器将范围上的变量设置为调用返回promise对象的服务方法的结果:
var MyController = function($scope, service) {
$scope.myVar = service.getStuff();
}
服务内部:
function getStuff() {
return $http.get( 'api/stuff' ).then( function ( httpResult ) {
return httpResult.data;
} );
}
这在我的angularjs应用程序的上下文中工作正常,但在茉莉花单元测试中不起作用。我已确认"然后"回调正在单元测试中执行,但$ scope.myVar承诺永远不会被设置为回调的返回值。
我的单元测试:
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;
});
}
为什么promise单元测试中的promise回调结果值没有传播到$ scope.myVar?有关完整的代码http://jsfiddle.net/s7PGg/5/
,请参阅以下jsfiddle答案 0 :(得分:21)
我想这个“神秘”的关键在于AngularJS会自动解析promises(和渲染结果),如果在模板中的插值指令中使用的那些。我的意思是给这个控制器:
MyCtrl = function($scope, $http) {
$scope.promise = $http.get('myurl', {..});
}
和模板:
<span>{{promise}}</span>
AngularJS,在$ http调用完成后,将“看到”承诺已解决,并将使用已解析的结果重新呈现模板。这是$q documentation中含糊其词的内容:
$ q promises被模板引擎识别为angular 意味着在模板中,您可以将附加到范围的承诺视为 如果它们是结果值。
可以看到发生这种魔法的代码here。
但是,只有当模板($parse
服务更加精确)在游戏中时才会发生这种“神奇”。 在您的单元测试中,没有涉及模板,因此承诺解析不会自动传播。
现在,我必须说这种自动分辨率/结果传播非常方便,但可能会让人感到困惑,正如我们从这个问题中看到的那样。这就是为什么我更喜欢像你一样明确地传播分辨率结果:
var MyController = function($scope, service) {
service.getStuff().then(function(result) {
$scope.myVar = result;
});
}
答案 1 :(得分:8)
我遇到了类似的问题,并让我的控制器将$ scope.myVar直接分配给promise。然后在测试中,我链接了另一个承诺,当它得到解决时断言承诺的期望值。我使用了这样的辅助方法:
var expectPromisedValue = function(promise, expectedValue) {
promise.then(function(resolvedValue) {
expect(resolvedValue).toEqual(expectedValue);
});
}
注意根据您调用expectPromisedValue的时间顺序以及当您的代码在测试中解析时,您可能需要手动触发最终的摘要周期才能运行然后()方法开始 - 没有它你的测试可能会通过,无论resolvedValue
是否等于expectedValue
。
为安全起见,请将触发器置于afterEach()调用中,这样您就不必为每次测试都记住它:
afterEach(inject(function($rootScope) {
$rootScope.$apply();
}));
答案 2 :(得分:3)
@ pkozlowski.opensource回答了原因(谢谢!),但不是如何在测试中绕过它。
我刚刚得到的解决方案是测试HTTP在服务中被调用,然后监视控制器测试中的服务方法并返回实际值而不是promises。
假设我们有一个与我们的服务器通信的用户服务:
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: 'charlie@example.com' }
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';
无需解决承诺。希望这会有所帮助。