javascript Angularjs 承诺不会在单元测试中得到解决
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/15048132/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Angularjs promise not being resolved in unit test
提问by robbymurphy
I am using jasmine to unit test an angularjs controller that sets a variable on the scope to the result of calling a service method that returns a promise object:
我正在使用 jasmine 对 angularjs 控制器进行单元测试,该控制器将作用域上的变量设置为调用返回承诺对象的服务方法的结果:
var MyController = function($scope, service) {
$scope.myVar = service.getStuff();
}
inside the service:
服务内部:
function getStuff() {
return $http.get( 'api/stuff' ).then( function ( httpResult ) {
return httpResult.data;
} );
}
This works fine in the context of my angularjs application, but does not work in the jasmine unit test. I have confirmed that the "then" callback is executing in the unit test, but the $scope.myVar promise never gets set to the return value of the callback.
这在我的 angularjs 应用程序的上下文中工作正常,但在 jasmine 单元测试中不起作用。我已经确认“then”回调正在单元测试中执行,但 $scope.myVar 承诺永远不会设置为回调的返回值。
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" );
} );
} );
Also, if I change the controller to the following the unit test passes:
此外,如果我将控制器更改为以下单元测试通过:
var MyController = function($scope, service) {
service.getStuff().then(function(result) {
$scope.myVar = result;
});
}
Why is the promise callback result value not being propagated to $scope.myVar in the unit test? See the following jsfiddle for full working code http://jsfiddle.net/s7PGg/5/
为什么在单元测试中没有将 promise 回调结果值传播到 $scope.myVar?有关完整的工作代码,请参阅以下 jsfiddle http://jsfiddle.net/s7PGg/5/
采纳答案by pkozlowski.opensource
I guess that the key to this "mystery" is the fact that AngularJS will automatically resolve promises (and render results) if those used in an interpolation directive in a template. What I mean is that given this controller:
我想这个“谜团”的关键在于,如果在模板的插值指令中使用了那些承诺(和呈现结果),AngularJS 将自动解决这些承诺(和呈现结果)。我的意思是,给定这个控制器:
MyCtrl = function($scope, $http) {
$scope.promise = $http.get('myurl', {..});
}
and the template:
和模板:
<span>{{promise}}</span>
AngularJS, upon $http call completion, will "see" that a promise was resolved and will re-render template with the resolved results. This is what is vaguely mentioned in the $q documentation:
AngularJS 在 $http 调用完成后,将“看到”承诺已解决,并将使用已解决的结果重新渲染模板。这是$q 文档中模糊提到的内容:
$q promises are recognized by the templating engine in angular, which means that in templates you can treat promises attached to a scope as if they were the resulting values.
$q 承诺被模板引擎以 angular 识别,这意味着在模板中,您可以将附加到范围的承诺视为结果值。
The code where this magic happens can be seen here.
可以在此处查看发生这种魔法的代码。
BUT, this "magic" happens only when there is a template ($parse
service, to be more precise) at play. In your unit test there is no template involved so promise resolution is not propagated automatically.
但是,这种“魔法”只有在有模板($parse
更准确地说是服务)在起作用时才会发生。在您的单元测试中,不涉及模板,因此不会自动传播承诺解析。
Now, I must say that this automatic resolution / result propagation is very convenient but might be confusing, as we can see from this question. This is why I prefer to explicitly propagate resolution results as you did:
现在,我必须说这种自动解析/结果传播非常方便,但可能会令人困惑,正如我们从这个问题中看到的那样。这就是为什么我更喜欢像你一样明确地传播解析结果:
var MyController = function($scope, service) {
service.getStuff().then(function(result) {
$scope.myVar = result;
});
}
回答by Kevin McCloskey
I had a similar problem and left my controller assigning $scope.myVar directly to the promise. Then in the test, I chained on another promise that asserts the expected value of the promise when it gets resolved. I used a helper method like this:
我遇到了类似的问题,让我的控制器将 $scope.myVar 直接分配给了 promise。然后在测试中,我链接了另一个承诺,当它得到解决时,它会断言承诺的预期值。我使用了这样的辅助方法:
var expectPromisedValue = function(promise, expectedValue) {
promise.then(function(resolvedValue) {
expect(resolvedValue).toEqual(expectedValue);
});
}
Notethat depending on the ordering of when you call expectPromisedValue and when the promise is resolved by your code under test, you may need to manually trigger a final digest cycle to run in order to get these then() methods to fire - without it your test may pass regardless of whether the resolvedValue
equals the expectedValue
or not.
请注意,根据您调用 expectPromisedValue 的顺序以及您的被测代码何时解决承诺,您可能需要手动触发最终摘要循环以运行这些 then() 方法 - 如果没有它,您的无论是否resolvedValue
等于,测试都可能通过expectedValue
。
To be safe, put the trigger in an afterEach() call so you don't have to remember it for every test:
为安全起见,将触发器放在 afterEach() 调用中,这样您就不必在每次测试时都记住它:
afterEach(inject(function($rootScope) {
$rootScope.$apply();
}));
回答by Christian Smith
@pkozlowski.opensource answered the why (THANK YOU!), but not how to get around it in testing.
@pkozlowski.opensource 回答了原因(谢谢!),但没有回答如何在测试中解决它。
The solution I just arrived at is to test that HTTP is getting called in the service, and then spy on the service methods in the controller tests and return actual values instead of promises.
我刚刚得到的解决方案是测试服务中是否调用了 HTTP,然后监视控制器测试中的服务方法并返回实际值而不是承诺。
Suppose we have a User service that talks to our server:
假设我们有一个与我们的服务器通信的用户服务:
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 that service, we don't care what happens to the returned values, only that the HTTP calls were made correctly.
测试该服务,我们不关心返回值会发生什么,只关心 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()
Now in our controller tests, there's no need to worry about HTTP. We only want to see that the User service is being put to work.
现在在我们的控制器测试中,无需担心 HTTP。我们只想看到 User 服务正在投入使用。
angular.module('app.controllers')
.controller('UserCtrl', function ($scope, $routeParams, User) {
$scope.user = User.get($routeParams.handle);
});
To test this, we spy on the User service.
为了测试这一点,我们监视用户服务。
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';
No need to resolve the promises. Hope this helps.
无需解决承诺。希望这可以帮助。