フロントエンドの石井と申します。
好きなゲームの種類はハクスラとローグライクです。 最近は Crypt of the NecroDancer に挫折して、Tales of Maj'Eyal へ河岸を変えました。
さて、弊社は Web アプリケーションのフレームワークとして主に Rails を採用しており、そのためブラウザで実行する JavaScript コードは、基本的には Sprokets (実際には sprockets-rails という Rails 用のインターフェース)を介してビルドされています。
この環境で、せめて各画面共通で使うモジュールにはユニットテストを書きたいと思い、Node.js 製ツール群の力を借りてそれを可能にしてみた、という話です。
ゴール
- Rails 4 デフォルト設定である、Sprockets と CoffeeScript による JS のビルドチェーンは変更しない
- ブラウザ環境で、ビルド後の
application-{digest}.js
が読み込まれた後のwindow
変数に対して、テストを書くことができる - テストファイル・テストケースは、mocha 形式で書くことができる
- 任意の単位でテストファイルを分割できる
- テストは CLI から実行することができ、適切な出力や exit ステータスを返すことができる
- テスト結果のレポートファイルを JUnit 形式で生成することができる
テスト方法の概要
三行説明します。
- a)
rake assets:precompile
でapplication-{digest}.js
を生成する - b)
application-{digest}.js
を Rails と無関係な静的な HTML 内で読み込む - c) その HTML にテストファイルを加え、ブラウザで実行した結果をどうにか CLI から取得する
あっ、辛そう!みたいに見えますが、(b) (c) の大半の部分は、karma というライブラリとそのプラグイン群が解決してくれます。
本記事では扱わない論点
- 一般的なミドルウェアや npm 操作方法の解説
- ググれば誰でもわかりそう、と判断した内容は飛ばしてしまいます
- Sprokets ではなく Node.js ツール群で JS のビルドを行う、いわゆる「モダンな」手法についての言及やそれとの比較
導入手順
はじめに
- 随所で、別途用意した サンプルリポジトリ のコミット等へのリンクを貼って説明します
- 今は確認する必要はありませんが、具体的なライブラリのバージョンなどが気になる場合は、そちらを参照してください
- Rails のバージョンだけ書いておくと
4.2.8
です
Node.js をインストールする
ユニットテストを実行したい環境に Node.js をインストールする必要があります。
バージョンは、6 系 or 7 系の新しい方であれは、おそらくは動きます。
もし動かなかったら、自分は 6.8.0
で検証していたのでそれを選択して下さい。
PhantomJS をインストールする
Node.js と同じように、実行したい環境へ PhantomJS をインストールする必要があります。
npm 環境を設定する
作業開始です。何はともあれ npm 環境の設定をするところからです。
Rails プロジェクトルートに package.json
というファイルを設置することで、npm 環境が設定されたことになります。
npm init
から新規作成できるので、対話的なインターフェースを経て生成してください。
入力内容は、今回の作業に当っては、全部空値のまま Enter で進んでしまって問題ありません。
完了後は、以下の差分のような package.json
が生成されていると思います。
環境によって微妙に結果が異なると思うので、厳密に一致している必要はありません。
またこのタイミングで、今後 npm の操作により生成されるファイル群を VCS から除外しておくと良いでしょう。
例えば、.gitignore
なら以下です。
クライアント用のディレクトリを作成する
テストファイル等を格納する場所のために、Rails プロジェクトルートに client
というディレクトリを作成してください。
mkdir client
ディレクトリ名が client
である理由は特に無いので、frontend
でも neko
でも何でも構いません。
application-{digest}.js をコピーする gulp タスクを作る
今回、テストをするにあたって Rails 世界から取得すべきデータは、 rake assets:precompile
でビルドした application-{digest}.js
のみです。
ここで、Node.js 側が Rails 側を不要に参照しなくて良いように、それを client
下にコピーしてくることにします。
また、今後の利便性のために digest 部分を削除するということも同時に行うようにもしましょう。
さて、copy-application-js-without-digest
という gulp タスクを作成したので、以下を実行してみましょう。
rake assets:precompile $(npm bin)/gulp copy-application-js-without-digest
precompile
によって生成された public/assets/application-{digest}.js
が、client/spec/built/application.js
へコピーされると思います。
加えて、個人的趣味の範疇ですが、上記を含むいくつかのコマンドを npm スクリプト化しておきました。
npm run test が動くようにする
本記事の最も主要な部分で、上記「テスト方法の概要」で「karma というライブラリがやってくれる」と記述した部分の設定です。
とは言っても、設定だけなので、先に作業内容を書いて、その補足だけします。
karma
とkarma-cli
をインストールすると、karma
コマンドが使えるようになるkarma.conf.js
はその設定ファイルで、karma
コマンド時にデフォルトで反映されるframeworks
は、何形式のテストの書式を解釈するのかを指定します。今回はmocha
を指定しており、karma-mocha
パッケージはそのために必要です。browsers
は、テストを実行するブラウザを指定します。今回はPhantomJS
を指定しており、karma-phantomjs-launcher
パッケージはそのために必要です。files
は、ブラウザが読み込む JS ファイルリストを指定します。現在はテスト対象のapplication.js
のみを読み込んでおり、テストファイルはまだ指定していません。
karma start --single-run
が CLI 用のインターフェースです。--single-run
を外すとテスト用のサーバが起動しますが、今回は使いません。
これで、application.js
を読み込んで 何かを判定を出来る、npm run test
が一応動くようになりました。
実行すると、
npm run test (省略) 10 04 2017 18:09:33.913:INFO [karma]: Karma v1.6.0 server started at http://0.0.0.0:9876/ 10 04 2017 18:09:33.916:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency 10 04 2017 18:09:33.925:INFO [launcher]: Starting browser PhantomJS 10 04 2017 18:09:34.395:INFO [PhantomJS 2.1.1 (Mac OS X 0.0.0)]: Connected on socket J0EGN991wi87-oBQAAAA with id 79802957 PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 0 of 0 ERROR (0.001 secs / 0 secs) (省略)
コマンドは動いているようですが、結果はエラーになりますね。 これはまだテストケースが無いためです。
テストを書く
試しに window
直下に関数やクラスを定義して、それらのテストを書いてみることにしましょう。
まず、addNumbers
というグローバル関数を定義し、そのテストを書いてみます。
補足として、chai というライブラリが足されていますが、これはテスト用にアサーションを行うためのライブラリです。
mocha
には、アサーションの機能が含まれていないので、別途ライブラリが必要になります。
続けて、CoffeeScript で Calculator
というクラスを定義し、そのテストを書いてみます。
実行してみましょう。
npm run test (略) I, [2017-04-10T17:52:45.529516 #5643] INFO -- : Removed /Users/ishii/app/js-unit-test-on-rails/public/assets 10 04 2017 17:52:50.617:INFO [karma]: Karma v1.6.0 server started at http://0.0.0.0:9876/ 10 04 2017 17:52:50.620:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency 10 04 2017 17:52:50.663:INFO [launcher]: Starting browser PhantomJS 10 04 2017 17:52:51.496:INFO [PhantomJS 2.1.1 (Mac OS X 0.0.0)]: Connected on socket xM8e_CNupOwTKha7AAAA with id 88364038 PhantomJS 2.1.1 (Mac OS X 0.0.0): Executed 2 of 2 SUCCESS (0.005 secs / 0.001 secs)
Executed 2 of 2 SUCCESS
から、2 テストケース実行されて全て成功したことがわかります。
コマンドラインの出力を mocha 形式にする
これは特には必要ないのですが、reporters
を指定して、実行時に結果が見やすいように mocha 形式に整形してみます。
実行すると、出力が変わっています。
npm run test (省略) START: 10 04 2017 18:36:19.140:INFO [karma]: Karma v1.6.0 server started at http://0.0.0.0:9876/ 10 04 2017 18:36:19.142:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency 10 04 2017 18:36:19.153:INFO [launcher]: Starting browser PhantomJS 10 04 2017 18:36:19.617:INFO [PhantomJS 2.1.1 (Mac OS X 0.0.0)]: Connected on socket XgKlhCbWjfJ40wlrAAAA with id 3070532 addNumbers ✔ 2 つの数値が加算できる Calculator add ✔ 数値が加算できる Finished in 0.006 secs / 0.001 secs @ 18:36:19 GMT+0900 (JST) SUMMARY: ✔ 2 tests completed
JUnit 形式のファイルを出力する
npm run test
時に、指定位置に JUnit 形式のレポートファイルを出力するようにします。
実行後に、このようなファイルも出力されるようになります。
tree tmp/reports tmp/reports └── TESTS-PhantomJS_2.1.1_(Mac_OS_X_0.0.0).xml
これで、「ゴール」に指定した要件は達成しました。
ローカルの Web サーバから application.js を取得する経路を作る
もうひと工夫します。
npm run test
を実行してみるとわかりますが、実行が遅いです。
実行時間が長いとつい Twitter とかを見ちゃう輩も居ますのですみません、この点を改善します。
遅いことの主な理由は rake assets:precompile
に時間が掛かるためです。また、rake assets:clobber
も「ファイルを消すだけじゃないの?」とは思うんですが、実際それなりに時間が掛かります。
ということで、これらを実行しないように、起動中のローカルの Web サーバからビルド済みの application.js
を取得する経路を作ることにします。
gulp copy-application-js-from-server
という、ローカルの Web サーバからダウンロードしてくるタスクを作り、
それを組み込んだ npm run test:run-without-precompile
という、別のテストコマンドを作成します。
実行時間を比較してみましょう。
後者は、実行前に rails server
で Web サーバを起動して下さい。
time npm run test (省略) npm run test 7.83s user 1.26s system 101% cpu 8.974 total
time npm run test:run-without-precompile (省略) npm run test:run-without-precompile 3.04s user 0.48s system 104% cpu 3.370 total
precompile
より、ずっとはやい!!
導入に当って予測される問題
最も問題になる点は、本来は Rails サーバが描画する HTMLに対して読み込まれる application-{digest}.js
を、全く別の静的な HTML 内で読み込むことだと思います。
おそらくは、サーバが出力する特定の JS 変数 や DOM 要素が無いと JS エラーになるような箇所がいくつかあるでしょう。
そのため、少なくとも読み込み時点ではエラーにならないように、例えば JS 変数ならモックする変数をその環境で定義するなどのワークアラウンドが必要になります。
ただ、幸いにもというか邪悪にもというか、jQuery は $('.存在しない要素').on()
の実行をエラーにしないので、DOM 側については修正が必要な箇所はさほど多くはないとは思います。
最後に
マネーフォワードでは、エンジニアを募集しています。 ご応募お待ちしています。
【採用サイト】 ■マネーフォワード採用サイト ■Wantedly | マネーフォワード
【プロダクト一覧】 自動家計簿・資産管理サービス『マネーフォワード』 ■Web ■iPhone,iPad ■Android
ビジネス向けクラウドサービス『MFクラウドシリーズ』 ■会計ソフト『MFクラウド会計』 ■確定申告ソフト『MFクラウド確定申告』 ■請求書管理ソフト『MFクラウド請求書』 ■給与計算ソフト『MFクラウド給与』 ■経費精算ソフト『MFクラウド経費』 ■入金消込ソフト『MFクラウド消込』 ■マイナンバー管理ソフト『MFクラウドマイナンバー』 ■資金調達サービス『MFクラウドファイナンス』
メディア ■くらしの経済メディア『MONEY PLUS』