Подтвердить что ты не робот

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/

4b9b3361

Ответ 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. Надеюсь, это поможет.