AngularJSでInterceptorでHeaderを付与するようなケースのテスト

例えば$httpのリクエストをインターセプトして、Authorizationヘッダーにトークンを付与するInterceptorを作成したとする。

この場合、このInterceptorのサービスをどのようにテストするのがよいのか?

で、調べたら大変素晴らしい記事があったのでそちらを参考にテストを書いた。

トークンを管理しているサービス、Authorizationヘッダーを付与するInterceptorサービス、モックの$httpBackend,あとはInterceptorが登録されているはずの $httpProviderを読み込んでる。

# auth_interceptor_spec.coffee
'use strict'

describe 'Service: AuthInterceptor', ->
  $httpProvider = {}
  beforeEach module 'gambaApp', (_$httpProvider_)->
    $httpProvider = _$httpProvider_
    return

  AuthInterceptor = {}
  AuthToken = {}
  $httpBackend = {}
  beforeEach inject (_AuthInterceptor_, _AuthToken_, _$httpBackend_) ->
    $httpBackend = _$httpBackend_
    AuthInterceptor = _AuthInterceptor_
    AuthToken = _AuthToken_
  
  afterEach inject (_AuthToken_) ->
    _AuthToken_.clear()

  token = 'SecureToken'
  tokenString = "Token token=\"#{token}\""

  it "AuthInterceptorが定義されていること", ->
    expect(AuthInterceptor).toBeDefined()

  it "AuthInterceptorがインターセプターに登録されていること", ->
    expect($httpProvider.interceptors).toContain "AuthInterceptor"

  it 'トークンがセットされていない場合Authorizationがセットされていないこと', ->
    config = AuthInterceptor.request({headers: {}})
    expect(config.headers.Authorization).toBeUndefined()

  it 'トークンが保持されている場合setting後Authorizationにセットされること', ->
    AuthToken.setToken(token)
    config = AuthInterceptor.request({})
    expect(config.headers.Authorization).toBe tokenString

  it 'トークンが保持されて通信されるときはAuthorizationにセットされていること', ->
    AuthToken.setToken(token)
    $httpBackend.whenGET('/api/users', (headers)->
      expect(headers.Authorization).toBe tokenString
    )

参考

UI-Bootstrapのmodalのテストで、$modalInstanceを使うControllerをテストしようとして、$modalInstanceをinjectしようとするところでUnknown providerとエラーが出る

公式のサンプルでいうと、ModalInstanceCtrl部分のテスト。

// modal-demo.js
var ModalDemoCtrl = function ($scope, $modal, $log) {

  $scope.items = ['item1', 'item2', 'item3'];

  $scope.open = function (size) {

    var modalInstance = $modal.open({
      templateUrl: 'myModalContent.html',
      controller: ModalInstanceCtrl,
      size: size,
      resolve: {
        items: function () {
          return $scope.items;
        }
      }
    });

    modalInstance.result.then(function (selectedItem) {
      $scope.selected = selectedItem;
    }, function () {
      $log.info('Modal dismissed at: ' + new Date());
    });
  };
};

// Please note that $modalInstance represents a modal window (instance) dependency.
// It is not the same as the $modal service used above.

var ModalInstanceCtrl = function ($scope, $modalInstance, items) {

  $scope.items = items;
  $scope.selected = {
    item: $scope.items[0]
  };

  $scope.ok = function () {
    $modalInstance.close($scope.selected.item);
  };

  $scope.cancel = function () {
    $modalInstance.dismiss('cancel');
  };
};

beforeEachで$modalInstanceをinjectしようとしているところで

#modal_instance_spec.coffee
"use strict"

describe "Controller: ModalInstanceCtrl", ->

  # load the controller"s module
  beforeEach module "myApp"

  ModalInstanceCtrl = {}
  scope = {}
  modalInstance = {}
  httpBackend = {}
  
  # Initialize the controller and a mock scope
  beforeEach inject ($controller, $rootScope, _$httpBackend_,_$modalInstance_) ->
    scope = $rootScope.$new()

こんなエラー

Error: [$injector:unpr] Unknown provider: $modalInstanceProvider <- $modalInstance

こんなときはmodalInstanceをmockにしてしまえばOK.

#modal_instance_spec.coffee
"use strict"

describe "Controller: ModalInstanceCtrl", ->

  # load the controller"s module
  beforeEach module "myApp"

  ModalInstanceCtrl = {}
  scope = {}
  modalInstance = {}
  httpBackend = {}
  
  # Initialize the controller and a mock scope
  beforeEach inject ($controller, $rootScope, _$httpBackend_) ->
    scope = $rootScope.$new()
    httpBackend = _$httpBackend_
    modalInstance =
      close: jasmine.createSpy("modalInstance.close")
      dismiss: jasmine.createSpy("modalInstance.dismiss")
      result: 
        then: jasmine.createSpy('modalInstance.result.then')

    ModalInstanceCtrl = $controller "ModalInstanceCtrl", {
      $scope: scope
      $modalInstance: modalInstance
    }

  it "cancelを呼ぶと、modalをdismissすること", ->
    scope.cancel()
    expect(modalInstance.dismiss).toHaveBeenCalled()
#........

参考というかそのまま

Rails4をJSON APIとして構築していてCreateのAPIに関連のID一覧をparameterとしてPOSTしてはまった

前提

1.Railsのwrap_parametersはJSONRailsへ送った時にルート要素を省いてくれる。というか、省いて送ってもよしなにwrapしてくれる。

Railsのwrap_parametersは何をしてくれるのか?

2.has_manyを定義するとrelation_ids,relation_ids=というメソッドが使えるようになる

#group.rb
Class Group
  has_many :groups_users
  has_many :users, through: :groups_users
end
#user.rb
class User
  has_many :groups_users
  has_many :groups, through: :groups_users
end
#irb
group = Group.first
group.user_ids
# =>[1,2,3]
group.user_ids=[2,3,4]

3.Railsのstrong_parametersは許可されたパラメータ以外を取り除く

#groups_controller.rb
class GroupsController < ApplicationController
  def create
    @group = Group.new(group_params)
    # 省略
  end

  private
  def group_params
    params.require(:group).permit(:name, user_ids: [])
  end
end

問題

このgroups#createに対して以下のようなJSONを送付したところ、groupモデルは作成されたが、userとの関連は作成されなかった。

// json
{
  name: "新規グループ",
  user_ids: [1,2,3]
}

user_idsがstrong_parametesに書いたにもかかわらず弾かれる。

実際のparamsを見るとこんな感じ

 Parameters: {"name"=>"新規グループ", "user_ids"=>[1,2,3], "group"=>{"name"=>"新規グループ"}}

調べるとどうもnested_attributes_forも引っかかるようだ。

解決

strong_paramtersのREADMEに書いてあった。

以下のようにrequire(:group)を取り除く

# groups_controller.rb
class GroupsController < ApplicationController
  # 省略

  private
  def group_params
    params.permit(:name, user_ids: [])
  end
end

これで、wrap_parametersもきき、groupとuserの関連も作成される。

grunt-wiredep使用の環境でjquery-uiのdatepickerのロケールとCSSを含みたい

grunt-wiredep使用の環境でAngularJSのui-dateロケール指定したい&jquery-uiのCSSを当てたいと。

bower.jsonに追加してbower install

bower.json

{
  "dependencies": {
    "angular-ui-date": "latest"
  }
}

ui-dateがjquery-uiに依存しているので、jquery-uiも入る。

jquery-uiの構成を確認すると、ロケールはui/i18n/datepicker-ja.jsに、CSSのテーマはthemes/にある。

なので、プロジェクトのbower.jsonでmainをoverrideして、ロケールファイルとCSSを含める。

下記はsmoothnessにしてみた例。

bower.json

 "jquery-ui": {
      "main": [
        "jquery-ui.js",
        "ui/i18n/datepicker-ja.js",
        "themes/smoothness/jquery-ui.css"
      ]
    }

grunt serveとか実行するとこうなる↓

<!-- index.html -->

  <!-- 省略 -->

  <!-- build:css(.) styles/vendor.css -->
  <!-- bower:css -->
  <link rel="stylesheet" href="bower_components/jquery-ui/themes/smoothness/jquery-ui.css" />
  <!-- endbower -->
  <!-- endbuild -->

  <!-- 省略 -->

  <!-- build:js(.) scripts/vendor.js -->
  <!-- bower:js -->

  <!-- 省略 -->
  <script src="bower_components/jquery-ui/jquery-ui.js"></script>
  <script src="bower_components/jquery-ui/ui/i18n/datepicker-ja.js"></script>
  <script src="bower_components/angular-ui-date/src/date.js"></script>
  <!-- endbower -->
  <!-- endbuild -->

あとはrunあたりで設定でOK

#app.coffee
myapp.run(->
  $.datepicker.setDefaults($.datepicker.regional[ "ja" ])
)

buildして製品用に吐き出す場合に、cssの読むimageも処理しなければならないが、grunt-contrib-copyなどでよしなにすればOK。

grunt-wiredep使用の環境でmomentの日本語ロケールファイルを含めたい

より正確にいうとgrunt-wiredep使用の環境でangular-momentロケール指定したい、ということ。

angular-momentをインストールする

% bower install angular-moment --save

これでmomentも依存関係でインストールされて、wiredepで差し込まれてめでたしめでたし…

index.html

  <!-- bower:js -->
  <!-- 略 -->
  <script src="bower_components/moment/moment.js"></script>
  <script src="bower_components/angular-moment/angular-moment.js"></script>
  <!-- endbower -->

いやいや、これだとロケールファイルが含まれていない。

bower_components/下のmomentを見ると、minディレクトリにmoment-with-locales.jsがある。

なので、プロジェクトのbower.jsonでmainをoverrideする。

bower.json

{
// 略
  "overrides": {
    "moment": {
      "main": "min/moment-with-locales.js"
    },
    "angular-i18n": {
      "main": "angular-locale_ja-jp.js"
    }
  }

ロケール入りになった。

index.html

  <!-- bower:js -->
  <!-- 略 -->
  <script src="bower_components/moment/min/moment-with-locales.js"></script>
  <script src="bower_components/angular-moment/angular-moment.js"></script>
  <!-- endbower -->

これで設定できる

app.coffee

myapp.run((amMoment) ->
    amMoment.changeLanguage('ja')
)

ProtractorでngMockE2Eを読み込んでモックを使ってテストしている場合、build時にはngMockE2Eを除外したい

yeomanのgenerator-angularで作ったプロジェクトで、grunt-wiredepを使っているので、素直にE2Eテストでangular-mocksを使おうとすると、bower.jsonのdevDependenciesからdependenciesに移す必要がある。

// bower.json
{
  "name": "MyApp",
  "version": "0.0.1",
  "dependencies": {
    "angular": "1.2.23",
    "angular-resource": "1.2.23",
    "angular-cookies": "1.2.23",
    "angular-sanitize": "1.2.23",
    "angular-animate": "1.2.23",
    "angular-touch": "1.2.23",
    "angular-ui-router": "~0.2.11",
    "angular-i18n": "~1.2.23"
  },
  "devDependencies": {
    "angular-mocks": "1.2.23"    // dependenciesに移すと…
  },
  "overrides": {
    "angular-i18n": {
      "main": "angular-locale_ja-jp.js"
    }
  },
  "appPath": "app"
}

しかし、これだとテスト時はいいが、grunt buildして製品用に吐き出した時にangular-mocks.jsも一緒に結合、圧縮されてしまう。

<!-- index.html -->
  <!-- build:js(.) scripts/vendor.js -->
  <!-- bower:js -->
  <script src="bower_components/jquery/dist/jquery.js"></script>
  <script src="bower_components/ng-file-upload-shim/angular-file-upload-shim.js"></script>
  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-resource/angular-resource.js"></script>
  <script src="bower_components/angular-cookies/angular-cookies.js"></script>
  <script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
  <script src="bower_components/angular-animate/angular-animate.js"></script>
  <script src="bower_components/angular-touch/angular-touch.js"></script>
  <script src="bower_components/angular-ui-router/release/angular-ui-router.js"></script>
  <script src="bower_components/angular-i18n/angular-locale_ja-jp.js"></script>
  <script src="bower_components/angular-mocks/angular-mocks.js"></script>
  <!-- wiredepにより、ここに挿入されるので、一緒にまとめられてしまう!! --> 
  <!-- endbower -->
  <!-- endbuild -->

何かいい方法はないかと調べたら、そのものずばりであった。

…And $httpBackend Mock For All (Unit & E2E) Testings

ので、下のようにを、ファイル名指定なしで書いてangular-mocks.jsを囲う。

  <!-- index.html -->
  <!-- build:js(.) scripts/vendor.js -->
  <!-- bower:js -->
  <script src="bower_components/jquery/dist/jquery.js"></script>
  <script src="bower_components/ng-file-upload-shim/angular-file-upload-shim.js"></script>
  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-resource/angular-resource.js"></script>
  <script src="bower_components/angular-cookies/angular-cookies.js"></script>
  <script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
  <script src="bower_components/angular-animate/angular-animate.js"></script>
  <script src="bower_components/angular-touch/angular-touch.js"></script>
  <script src="bower_components/angular-ui-router/release/angular-ui-router.js"></script>
  <script src="bower_components/angular-i18n/angular-locale_ja-jp.js"></script>
  <!-- endbower -->
  <!-- endbuild -->
  <!-- build:js -->
  <script src="bower_components/angular-mocks/angular-mocks.js"></script>
  <!-- endbuild -->
 ```

これでテスト時は普通にモックが使えて、grunt buildすると、angular-mocks.jsのセクションは消えている。

grunt-wiredep使用の環境でangular-i18nをbowerでインストールしたら注入できないとメッセージ

AngularJSリファレンス購入して読了。これは良い本。

で、フィルタのi18nの項をみて、早速いれてみたら、grunt-wiredepを使っているGruntのタスクで注入出来ないとメッセージが出て、htmlファイルのコメントの間にi18nへのscriptタグが挿入されない。

ちなみにgenerator-angularで作ったプロジェクトで、bowerでi18nを入れての話。

% bower install angular-i18n --save

% grunt serve

# .......略...............

Running "wiredep:app" (wiredep) task
app/index.html modified.

angular-i18n was not injected in your file.
Please go take a look in "/home/vagrant/myApp/client/bower_components/angular-i18n" for the file you need, then manually include it in your file.

Running "wiredep:sass" (wiredep) task

angular-i18n was not injected in your file.
Please go take a look in "/home/vagrant/myApp/client/bower_components/angular-i18n" for the file you need, then manually include it in your file.

index.html

  <!-- bower:js -->
  <!-- 略 -->
  <script src="bower_components/angular-i18n/angular-locale_ja-jp.js"></script>
  <!-- ↑これが入って欲しいが入らない -->
  <!-- endbower -->

angular-i18nのbower.jsonを確認するとmainが指定されていない。 まあ色々な言語のパッケージが入ってるし、勝手にロケールを解決してくれるとも思えないので、そりゃそうか。 なので、bower.jsonにこうした。

//bower.json
  "overrides": {
    "angular-i18n": {
      "main": "angular-locale_ja-jp.js"
    }
  }

これでgrunt serveなりwiredepのタスクが含まれたタスクを実行すると…OK.注入された。

index.html

  <!-- bower:js -->
  <!-- 略 -->  
  <script src="bower_components/angular-i18n/angular-locale_ja-jp.js"></script>
  <!-- endbower -->