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() #........
参考というかそのまま
Rails4をJSON APIとして構築していてCreateのAPIに関連のID一覧をparameterとしてPOSTしてはまった
前提
1.Railsのwrap_parametersはJSONをRailsへ送った時にルート要素を省いてくれる。というか、省いて送ってもよしなに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のセクションは消えている。