web-dev-qa-db-ja.com

単体テストでAngularjsの約束が解決されない

Jasmineを使用して、promiseオブジェクトを返すサービスメソッドを呼び出した結果にスコープの変数を設定するangularjsコントローラーの単体テストを行っています。

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/

21
robbymurphy

この "謎"の鍵は、AngularJSがプロミスを解決する(そして結果をレンダリングする)ことが、テンプレートの補間ディレクティブで使用されている場合に自動的に行われるという事実だと思います。つまり、このコントローラーを指定すると、

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

そしてテンプレート:

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

AngularJSは、$ http呼び出しの完了時に、promiseが解決されたことを「確認」し、解決された結果を含むテンプレートを再レンダリングします。これは $ qドキュメント で漠然と述べられているものです:

$ qプロミスはテンプレートエンジンによって角度で認識されます。つまり、テンプレートでは、スコープにアタッチされたプロミスを結果の値のように扱うことができます。

この魔法が発生するコードを見ることができます here

しかし、この「魔法」はテンプレート($parseサービス、より正確には)プレイ時。 ユニットテストにはテンプレートが含まれていないため、promiseの解決は自動的に反映されません

この自動解決/結果の伝達は非常に便利ですが、この質問からわかるように、混乱するかもしれません。これが私があなたがしたように解決結果を明示的に伝えることを好む理由です:

var MyController = function($scope, service) {
    service.getStuff().then(function(result) {
        $scope.myVar = result;
    });
}
21

同様の問題があり、コントローラーが$ scope.myVarを直接promiseに割り当てたままにしました。次に、テストでは、別のプロミスを連鎖させて、プロミスが解決されたときに期待される値を表明しました。私はこのようなヘルパーメソッドを使用しました:

var expectPromisedValue = function(promise, expectedValue) {
  promise.then(function(resolvedValue) {
    expect(resolvedValue).toEqual(expectedValue);
  });
}

expectPromisedValueを呼び出すときの順序と、テスト中のコードによってpromiseが解決されるときの順序によっては、then()を取得するために、最後のダイジェストサイクルを手動でトリガーして実行する必要がある場合があります。実行するメソッド-これがないと、resolvedValueexpectedValueと等しいかどうかに関係なく、テストに合格する可能性があります。

安全のために、トリガーをafterEach()呼び出しに入れて、すべてのテストでトリガーを覚える必要がないようにします。

afterEach(inject(function($rootScope) {
  $rootScope.$apply();
}));
8
Kevin McCloskey

@ pkozlowski.opensourceが理由(THANK YOU!)に回答しましたが、テストでそれを回避する方法は回答していません。

私がたどり着いた解決策は、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について心配する必要はありません。 Userサービスが機能していることを確認するだけです。

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';

約束を解決する必要はありません。お役に立てれば。

3
Christian Smith