仙台 Ruby Vim JavaScript 社長

片平堂のブログ

AngularJSのフォームで投稿後に再利用する際手付かずの状態に戻したい

例えば記事へのコメント投稿後に、そのフォームを再利用する際に、データを初期化しただけだとエラー表示が出てしまう。

ので、$dirtyがfalse($pristineがtrue)の状態に戻したい。

いわゆるブログのコメント投稿とかのよくあるやつ。

form.FormControllerの$setPristine()を使う。

# comment_new.html
  <div ng-controller="CommentNewCtrl">
    <form name="commentNewForm">
      <div class="form-group"
           ng-class="{'has-error': commentNewForm.content.$dirty && commentNewForm.content.$invalid }">
        <textarea class="form-control" ng-model="comment.content" placeholder="" tabindex="1" name="content"
                  required></textarea>
        <span ng-show="commentNewForm.content.$dirty&&commentNewForm.content.$error.required">
          必須です
        </span>
      </div>
      <button ng-disabled="!commentNewForm.$valid" ng-click="save()">コメントを投稿</button>
    </form>
  </div>
# comment_new.coffee
angular.module('myApp')
.controller 'CommentNewCtrl', ($scope, Comment) ->

# .........................

  $scope.save = ->
    $scope.comment.$create((data)->
      # .........................
      $scope.comment = new Comment(report_id: reportId) #<- 初期化
      $scope.commentNewForm.$setPristine()  # <- コレ
    )

OK.

RailsのResourcesでNestしてshallow: trueしたresourcesをAngularJSの$resourceで扱う

例えばこういう感じのresourcesを定義したとする。

APIの例。newとeditは省いた。

# routes.rb
namespace :api, defaults: { format: :json } do
  resources :reports, shallow: true do
    resources :comments, except: [:new, :edit]
  end
end

定義されるルーティングはこうなる。reportsのは話に関係ないので省略

GET  /api/reports/:report_id/comments(.:format)  api/comments#index {:format=>:json}

POST /api/reports/:report_id/comments(.:format) api/comments#create {:format=>:json}

GET /api/comments/:id(.:format) api/comments#show {:format=>:json}

PATCH   /api/comments/:id(.:format) api/comments#update {:format=>:json}

PUT /api/comments/:id(.:format) api/comments#update {:format=>:json}

DELETE  /api/comments/:id(.:format) api/comments#destroy {:format=>:json}

Railsではよくあるパターンだと思う。

このコメントのresourceをAngularJSの$resourceで使いたい。

$resourceでよく見かけるサンプルだとシンプルなものが多い.

こういったことは出来ないんだろうかと、AngularJSのリファレンスで$resourceの項目を見てみたら、ちゃんとやり方が書いてあった。

url – {string} – action specific url override. The url templating is supported just like for the resource-level urls.

$resourceの第3引数のactionsでurlをoverrideすればよいと。

やってみた。

下記はqueryとかsaveとか自動のやつは無視してRails的なactionを定義

# Comment.coffee
'use strict'

angular.module('myApp')
.factory 'Comment', ($resource) ->
  $resource("/api/comments/:commentId", {commentId: "@id", reportId: "@report_id"}, {
    index: {method: "GET", url: "/api/reports/:reportId/comments", isArray: true}
    create: {method: "POST", url: "/api/reports/:reportId/comments"}
    update: {method: "PATCH"}
    show: { method: "GET"}
    destroy: {method: "DELETE"}
  })

使う例

# sample.coffee
angular.module('myApp')
.controller "SampleCtrl", ($scope, $stateParams, Comment) ->
  $scope.comments = Comment.index(reportId: $stateParams.reportId)

  $scope.comment = new Comment(report_id: $stateParams.reportId)

  $scope.save = ->
    $scope.comment.$create((comment)->
      # do something
 

上記の場合だと、report_idとreportIdでもにょってしまうので、サービス側でもう少しラップしてオブジェクトの引数ではなく、パラメータで渡した方が幸せになれるかもしれない。

AngularJSで$sceを使ってるフィルタなんかのテスト

$sceをDIしてるやつ、例えばこちらのようなののテストを書く場合。

# newlines_spec.coffee
"use strict"

describe "Filter: newlines", ->

  beforeEach module "myApp"

  newlines = {}
  beforeEach inject ($filter) ->
    newlines = $filter "newlines"

  it "\nを<br />に変換して返すこと", ->
    text = "angularjs\nnewlines"
    expect(newlines(text)).toBe ("angularjs<br />newlines")

単純にこんな感じに書くと、下記のようなメッセージで失敗する

Expected { $$unwrapTrustedValue : Function } to be 'angularjs<br />newlines'.

ので、$$unwrapTrustedValueでexpectしてみた。

# newlines_spec.coffee
  it "\nを<br />に変換して返すこと", ->
    text = "angularjs\nnewlines"
    expect(newlines(text).$$unwrapTrustedValue()).toBe ("angularjs<br />newlines")

テスト通ったけど、$sceProviderをテストの時は無効化するとか、他のやり方のほうがいいんだろうか。

beforeEach module 'myApp',($sceProvider)->
  $sceProvider.enabled(false)
  return

RailsでつくるAPIサーバのドキュメントを自動生成してくれるAutodocを使っていて気をつけること

Autodocとはなんぞやという方はこちらの記事を参照。 公式はこちら

で、大変便利なAutodocだけど、注意点が2つ。

まず一点目はRspc3で動かない点。プルリクが上がってるが、取り込まれてない。

なので、forkして使ってる人が結構いる感じ。

2点目、通信が発生しているところでマークダウンが生成されるので、shared_exampleとか使って、そこで実際の通信が発生するようなものは、ドキュメントが全てshared_exampleがあるファイル名で生成されてしまう。

具体的に言うと、例えばこちらの記事のようなヘルパーメソッドを用意して、これでAUTODOC=1でAPIのテストを全て行った場合、shared_exmpleを呼び出している個別のspecファイルではなく、doc/support/api_helper.mdというドキュメントが生成されてしまう。

こうなるのは仕方がない気がするので、autodocを使ってドキュメントを生成する場合は、Autodocで記録する個別のexampleの中でgetやpostを投げる感じで。

AngularJSのProtractorでngMockE2Eの$httpBackentを使ってテストを書く際に気をつけること

まだ理解が浅い模様。

注意というかexpectとwhenの違いの話。

前提

  • AngularJSにはngMockE2EというE2E用のモックが用意されており、こちらを使用することにより、サーバを用意しなくてもモックでテストが書ける。

* ngMockE2Eの$httpBackendにはngMockの$httpBackendと違って、expect系はなくwhen系のメソッドしかない

問題

よくある入力フォームのE2EテストをAPIの呼び出し部分にモックを使って書いていて、正常な登録と、エラー表示をまとめてテストしようとして、うまくいかなかった。

# sign_in_spec.coffee
"use strict"

describe "ユーザ登録画面", ->
  beforeEach ->
    browser.addMockModule("httpBackendMock", ->
      angular.module("httpBackendMock", ["ngMockE2E"])
      .run(($httpBackend) ->
        $httpBackend.whenPOST("/api/sign_in",
          {
            user:
              email: "test@example.com"
              password: "password"
                password_confirmation: "password"
          }
        ).respond(201, {
            "user":
              "id": 480
          })
        $httpBackend.whenPOST("/api/sign_in",
          {
            user:
              email: "test@example.com"
              password: "password"
              password_confirmation: "12345678"
          }
        ).respond(422, {
            "status": 422,
            "messages": [
              "パスワード再確認とパスワードの入力が一致しません。"
            ]
          })
        $httpBackend.when("GET", /.*/).passThrough()
      )
    )

  beforeEach ->
    browser.get("/sign_in")

  it "エラー確認、登録後、別画面に遷移", ->
    email = element(By.model("registration.email"))
    password = element(By.model("registration.password"))
    password_confirmation = element(By.model("registration.password_confirmation"))
    button = element(By.tagName("button"))
    email.sendKeys("test")
    error = element(By.css(".form-group span:not([class=ng-hide])")).getText()
    expect(error).toBe "メールアドレスを入力してください"
    email.clear()
    error = element(By.css(".form-group span:not([class=ng-hide])")).getText()
    expect(error).toBe "メールアドレスは必須です"
    email.sendKeys("test@example.com")
    # .....省略.......
    password_confirmation.sendKeys("12345678")
    # この時点では、passwordとpassword_confirmationが違う事により失敗のメッセージのレスポンスを期待する
    button.click()
    expect(errorElement.getText())toBe "パスワード再確認とパスワードの入力が一致しません。"
    # .....省略.......
    # 正常系
    button.click()
    expect(browser.getLocationAbsUrl()).toMatch "#/other_page"
    

api/sign_inへのPOSTのバックエンド定義を2つ書いている。
ユーザパラメータの違いにより、使われるモックが変わることを期待しているが、片方しか呼ばれない。
これは考えてみれば当たり前で、whenはexpectと違って、定義されたリクエストが行われた場合に用意されたレスポンスを返し、リクエストを厳密にアサートしないバックエンドを定義するものだから。 パラメータやヘッダーなど、このようにリクエストしてほしいというアサーションを期待するのはexpect系を使わないと書けない。

ちなみに $httpBackend.when("GET", /.*/).passThrough()で、モック以外の通信(この場合、静的なHTMLの取得)を全てサーバと通信するようにしている。

解決

ngMockE2Eの$httpBackendにはexpectがないので、 正常系と失敗系でdescribeを分けて、それぞれにmockを定義してテストを行った。

# sign_in_spec.coffee
"use strict"

describe "ユーザ登録画面", ->
  beforeEach ->
    browser.addMockModule("httpBackendMock", ->
      angular.module("httpBackendMock", ["ngMockE2E"])
      .run(($httpBackend) ->
        $httpBackend.whenPOST("/api/sign_in",
          {
            user:
              email: "test@example.com"
              password: "password"
              password_confirmation: "password"
          }
        ).respond(201, {
            "user":
              "id": 480
          })
        $httpBackend.when("GET", /.*/).passThrough()
      )
    )

  beforeEach ->
    browser.get("/sign_up")

  it "登録後、別画面に遷移", ->
    # test code here

describe "ユーザ登録画面error", ->
  beforeEach ->
    browser.addMockModule("httpBackendMock", ->
      angular.module("httpBackendMock", ["ngMockE2E"])
      .run(($httpBackend) ->
        $httpBackend.whenPOST("/api/sign_in",
          {
            user:
              email: "test@example.com"
              password: "password"
              password_confirmation: "12345678"
          }
        ).respond(422, {
            "status": 422,
            "messages": [
              "パスワードを再入力とパスワードの入力が一致しません。"
            ]
          })

        $httpBackend.when("GET", /.*/).passThrough()
      )
    )

  beforeEach ->
    browser.get("/sign_up")

  it "エラー表示確認", ->
   # test code here    

expect系が用意されていないのは、統合テストという性格上、実際にテスト用のAPIサーバを用意して実際の挙動でテストするのを推奨していて、モックは限定的に使えという意味合いなのかなと思った。

今回、APIサーバはRailsだが、RailsにAngularJSを載せるのではなく、AngularJSとRailsの役割を完全に切り離して開発しているので、E2Eテストでどうしようかなと模索中。 実際にrails serverをテスト環境で動かしてproxyでつなぐのがいいかなと思いつつ、DBデータのテスト事の処理をどうしようかと。 なにかいい方法を知っている方がいたら教えていただきたい感じ。

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()
#........

参考というかそのまま