21

我正在使用 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 应用程序的上下文中工作正常,但在 jasmine 单元测试中不起作用。我已经确认“then”回调正在单元测试中执行,但 $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 回调结果值没有被传播到 $scope.myVar?有关完整的工作代码,请参见以下 jsfiddle http://jsfiddle.net/s7PGg/5/

4

3 回答 3

21

我猜这个“谜团”的关键在于,如果模板中的插值指令中使用了承诺,AngularJS 将自动解析承诺(并呈现结果)。我的意思是,给定这个控制器:

MyCtrl = function($scope, $http) {
  $scope.promise = $http.get('myurl', {..});
}

和模板:

<span>{{promise}}</span>

AngularJS 在 $http 调用完成后,将“看到”一个承诺已解决,并将使用已解决的结果重新呈现模板。这是$q 文档中模糊提到的内容:

$q promises 被模板引擎以角度识别,这意味着在模板中,您可以将附加到范围的 promises 视为结果值。

可以在这里看到发生这种魔法的代码。

但是,这种“魔法”只有在有模板($parse更准确地说是服务)在起作用时才会发生。在您的单元测试中没有涉及模板,因此不会自动传播承诺解决方案

现在,我必须说这种自动解析/结果传播非常方便,但可能会令人困惑,正如我们从这个问题中看到的那样。这就是为什么我更喜欢像您一样显式传播分辨率结果:

var MyController = function($scope, service) {
    service.getStuff().then(function(result) {
        $scope.myVar = result;
    });
}
于 2013-02-24T11:00:10.653 回答
8

我遇到了类似的问题,让我的控制器直接将 $scope.myVar 分配给了 Promise。然后在测试中,我链接了另一个 Promise,当它被解决时,它断言 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();
}));
于 2013-08-20T23:38:34.347 回答
3

@pkozlowski.opensource 回答了原因(谢谢!),但没有回答如何在测试中绕过它。

我刚刚得出的解决方案是测试服务中是否调用了 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: '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';

无需解决承诺。希望这可以帮助。

于 2013-03-22T20:10:14.080 回答