web-dev-qa-db-ja.com

Jasmine JSテストでAngularJS Promiseコールバックがトリガーされない

問題の紹介

Facebook JavaScript SDKFBオブジェクトをラップするAngularJSサービスを単体テストしようとしています。ただし、テストは機能せず、その理由を理解できませんでした。また、 JasmineJS 単体テストの代わりにブラウザーで実行すると、サービスコードは機能します。 Karmaテストランナー で実行します。

Angular $q オブジェクトを介したpromiseを使用して非同期メソッドをテストしています。 Jasmine 1.3.1非同期テストメソッド 、ただしwaitsFor()関数はtrue(以下のテストコードを参照)を返さず、ただタイムアウトします5秒(KarmaはJasmine 2.0非同期テストAPIをまだ出荷していません)。

then()を呼び出しているにもかかわらず、Promiseのconsole.log()メソッドがトリガーされない(それを示すために$scope.$apply()が設定されている)からだと思う]非同期メソッドが戻ったとき、Angular=ダイジェストサイクルを実行し、then()コールバックをトリガーする必要があることを知らせるために...しかし間違っている可能性があります。

これは、テストの実行から得られるエラー出力です。

Chrome 32.0.1700 (Mac OS X 10.9.1) service Facebook should return false
  if user is not logged into Facebook FAILED
  timeout: timed out after 5000 msec waiting for something to happen
Chrome 32.0.1700 (Mac OS X 10.9.1):
  Executed 6 of 6 (1 FAILED) (5.722 secs / 5.574 secs)

コード

これはサービスの私の単体テストです(これまでに見つけたものを説明するインラインコメントを参照してください):

'use strict';

describe('service', function () {
  beforeEach(module('app.services'));

  describe('Facebook', function () {
    it('should return false if user is not logged into Facebook', function () {
      // Provide a fake version of the Facebook JavaScript SDK `FB` object:
      module(function ($provide) {
        $provide.value('fbsdk', {
          getLoginStatus: function (callback) { return callback({}); },
          init: function () {}
        });
      });

      var done = false;
      var userLoggedIn = false;

      runs(function () {
        inject(function (Facebook, $rootScope) {
          Facebook.getUserLoginStatus($rootScope)
            // This `then()` callback never runs, even after I call
            // `$scope.$apply()` in the service :(
            .then(function (data) {
              console.log("Found data!");
              userLoggedIn = data;
            })
            .finally(function () {
              console.log("Setting `done`...");
              done = true;
            });
        });
      });

      // This just times-out after 5 seconds because `done` is never
      // updated to `true` in the `then()` method above :(
      waitsFor(function () {
        return done;
      });

      runs(function () {
        expect(userLoggedIn).toEqual(false);
      });

    }); // it()
  }); // Facebook spec
}); // Service module spec

そして、これは私のAngularテストされているサービスです(これまでに見つけたものを説明するインラインコメントを参照してください ):

'use strict';

angular.module('app.services', [])
  .value('fbsdk', window.FB)
  .factory('Facebook', ['fbsdk', '$q', function (FB, $q) {

    FB.init({
      appId: 'xxxxxxxxxxxxxxx',
      cookie: false,
      status: false,
      xfbml: false
    });

    function getUserLoginStatus ($scope) {
      var deferred = $q.defer();
      // This is where the deferred promise is resolved. Notice that I call
      // `$scope.$apply()` at the end to let Angular know to trigger the
      // `then()` callback in the caller of `getUserLoginStatus()`.
      FB.getLoginStatus(function (response) {
        if (response.authResponse) {
          deferred.resolve(true);
        } else {
          deferred.resolve(false)
        }
        $scope.$apply(); // <-- Tell Angular to trigger `then()`.
      });

      return deferred.promise;
    }

    return {
      getUserLoginStatus: getUserLoginStatus
    };
  }]);

資源

以下は、この問題を解決するためにすでに検討した他のリソースのリストです。

概要

私はすでに他のAngular Facebook JavaScript SDK用のライブラリがあることを知っていることに注意してください次のように:

Angular自分でサービスを書く方法を学びたかったので、今のところそれらを使用することに興味はありません。my codeの問題を修正し、他の誰かのを使用するよう提案する代わりに。

だから、それが言われていると、誰も私のテストが機能しない理由を知っていますか?

40
user456814

TL; DR

テストコードから$rootScope.$digest()を呼び出すと、パスします。

_it('should return false if user is not logged into Facebook', function () {
  ...

  var userLoggedIn;

  inject(function (Facebook, $rootScope) {
    Facebook.getUserLoginStatus($rootScope).then(function (data) {
      console.log("Found data!");
      userLoggedIn = data;
    });

    $rootScope.$digest(); // <-- This will resolve the promise created above
    expect(userLoggedIn).toEqual(false);
  });
});
_

Plunker ここ

注:run()およびwait()の呼び出しはここでは必要ないため(実際の非同期呼び出しは実行されないため)削除しました。

詳しい説明

ここで何が起こっているのか:getUserLoginStatus()を呼び出すと、内部でFB.getLoginStatus()を実行し、次にそのコールバックを実行しますimmediately。それを正確に行うために。ただし、$scope.$apply()呼び出しはそのコールバック内にあるため、テストの before .then()ステートメントが実行されます。 then()は新しいプロミスを作成するため、そのプロミスを解決するには新しいダイジェストが必要です。

この問題は、次の2つの理由のうち1つが原因でブラウザでは発生しないと考えています。

  1. FB.getLoginStatus()はすぐにコールバックを呼び出さないので、then()呼び出しは最初に実行されます。または
  2. アプリケーションの他の何かが新しいダイジェストサイクルをトリガーします。

そのため、明示的にテストするかどうかに関係なく、テスト内でプロミスを作成する場合は、そのプロミスを解決するために、ある時点でダイジェストサイクルをトリガーする必要があります。

52
Michael Benford
    'use strict';

describe('service: Facebook', function () {
    var rootScope, fb;
    beforeEach(module('app.services'));
    // Inject $rootScope here...
    beforeEach(inject(function($rootScope, Facebook){
        rootScope = $rootScope;
        fb = Facebook;
    }));

    // And run your apply here
    afterEach(function(){
        rootScope.$apply();
    });

    it('should return false if user is not logged into Facebook', function () {
        // Provide a fake version of the Facebook JavaScript SDK `FB` object:
        module(function ($provide) {
            $provide.value('fbsdk', {
                getLoginStatus: function (callback) { return callback({}); },
                init: function () {}
            });
        });
        fb.getUserLoginStatus($rootScope).then(function (data) {
            console.log("Found data!");
            expect(data).toBeFalsy(); // user is not logged in
        });
    });
}); // Service module spec

これはあなたが探していることをするはずです。 beforeEachを使用してrootScopeをセットアップし、afterEachを使用して適用を実行することで、テストが簡単に拡張できるようになり、ユーザーがログインしているかどうかのテストを追加できます。

5
var-foo

私があなたのコードが機能しない理由の問題からわかることから、あなたは$ scopeを挿入していないということです。 Michielsは$ rootScopeを注入し、ダイジェストサイクルを呼び出すため、動作します。ただし、$ apply()はダイジェストサイクルを呼び出すためのより高いレベルであるため、同様に機能しますが...サービス自体に注入する場合のみ。

しかし、サービスは$ scopeの子を作成しないので、$ rootScope自体を注入する必要があると思います-私が知っている限り、$ scopeをうまく作成するための仕事として$ scopeを注入できるのはコントローラーだけです。しかし、これは少し憶測です、私はそれについて100パーセント確信していません。ただし、アプリがng-appの作成から$ rootScopeを持っていることを知っているので、$ rootScopeを試してみます。

'use strict';

angular.module('app.services', [])
  .value('fbsdk', window.FB)
  .factory('Facebook', ['fbsdk', '$q', '$rootScope' function (FB, $q, $rootScope) { //<---No $rootScope injection

    //If you want to use a child scope instead then --> var $scope = $rootScope.$new();
    // Otherwise just use $rootScope

    FB.init({
      appId: 'xxxxxxxxxxxxxxx',
      cookie: false,
      status: false,
      xfbml: false
    });

    function getUserLoginStatus ($scope) { //<--- Use of scope here, but use $rootScope instead
      var deferred = $q.defer();
      // This is where the deferred promise is resolved. Notice that I call
      // `$scope.$apply()` at the end to let Angular know to trigger the
      // `then()` callback in the caller of `getUserLoginStatus()`.
      FB.getLoginStatus(function (response) {
        if (response.authResponse) {
          deferred.resolve(true);
        } else {
          deferred.resolve(false)
        }
        $scope.$apply(); // <-- Tell Angular to trigger `then()`. USE $rootScope instead!
      });

      return deferred.promise;
    }

    return {
      getUserLoginStatus: getUserLoginStatus
    };
  }]);
0
Sten Muchow