Angularjs не решается в unit test
Я использую жасмин в unit test контроллере angularjs, который устанавливает переменную области видимости в результате вызова метода службы, который возвращает объект обещания:
var MyController = function($scope, service) {
$scope.myVar = service.getStuff();
}
внутри службы:
function getStuff() {
return $http.get( 'api/stuff' ).then( function ( httpResult ) {
return httpResult.data;
} );
}
Это отлично работает в контексте моего приложения angularjs, но не работает в жасмине unit test. Я подтвердил, что "then" callback выполняется в unit test, но обещание $scope.myVar никогда не будет установлено на возвращаемое значение обратного вызова.
Мой 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" );
} );
} );
Кроме того, если я изменю контроллер на следующий unit test pass:
var MyController = function($scope, service) {
service.getStuff().then(function(result) {
$scope.myVar = result;
});
}
Почему значение результата обратного вызова обещания не распространяется на $scope.myVar в unit test? См. Следующий jsfiddle для полного рабочего кода http://jsfiddle.net/s7PGg/5/
Ответы
Ответ 1
Я полагаю, что ключом к этой "тайне" является тот факт, что AngularJS автоматически разрешит promises (и отобразит результаты), если те, которые используются в директиве интерполяции в шаблоне. Я имею в виду, что данный контроллер:
MyCtrl = function($scope, $http) {
$scope.promise = $http.get('myurl', {..});
}
и шаблон:
<span>{{promise}}</span>
AngularJS при завершении $http-вызова "увидит", что обещание было разрешено и будет повторно отображать шаблон с разрешенными результатами. Это то, что смутно упоминается в документации $q:
$q promises распознаются двигателем шаблонов в angular, который означает, что в шаблонах вы можете рассматривать promises, прикрепленные к области как если они были результирующими значениями.
Код, где происходит эта магия, можно увидеть здесь.
НО, эта "магия" происходит только тогда, когда в игре есть шаблон ($parse
service, если быть более точным). В вашем unit test нет шаблона, поэтому обещание не распространяется автоматически.
Теперь я должен сказать, что это автоматическое разрешение/распространение результатов очень удобно, но может быть запутанным, как мы можем видеть из этого вопроса. Вот почему я предпочитаю явно распространять результаты разрешения, как вы это делали:
var MyController = function($scope, service) {
service.getStuff().then(function(result) {
$scope.myVar = result;
});
}
Ответ 2
У меня была аналогичная проблема, и мой контроллер назначил $scope.myVar прямо к обещанию. Затем в тесте я приковал еще одно обещание, которое утверждает ожидаемое значение обещания, когда оно будет разрешено. Я использовал вспомогательный метод следующим образом:
var expectPromisedValue = function(promise, expectedValue) {
promise.then(function(resolvedValue) {
expect(resolvedValue).toEqual(expectedValue);
});
}
Примечание, что в зависимости от того, когда вы вызываете expectPromisedValue, и когда обещание будет разрешено вашим тестируемым кодом, вам может потребоваться вручную запустить окончательный цикл дайджеста для запуска, чтобы получить эти то() методы для стрельбы - без этого ваш тест может проходить независимо от того, равен ли resolvedValue
expectedValue
или нет.
Чтобы быть в безопасности, поставьте триггер в вызове afterEach(), поэтому вам не нужно запоминать его для каждого теста:
afterEach(inject(function($rootScope) {
$rootScope.$apply();
}));
Ответ 3
@pkozlowski.opensource ответил, почему (СПАСИБО!), но не как обойти его при тестировании.
Решение, к которому я только что пришел, - проверить, что HTTP получает вызов в службе, а затем шпионить за методами службы в тестах контроллера и возвращать действительные значения вместо promises.
Предположим, что у нас есть служба User, которая говорит с нашим сервером:
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';
Не нужно разрешать promises. Надеюсь, это поможет.