Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

Sprockets管理のJavaScriptを強引にユニットテスト

フロントエンドの石井と申します。

好きなゲームの種類はハクスラとローグライクです。 最近は 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:precompileapplication-{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 というライブラリがやってくれる」と記述した部分の設定です。

とは言っても、設定だけなので、先に作業内容を書いて、その補足だけします。

→ 作業内容

  • karmakarma-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 | マネーフォワード

【プロダクト一覧】 自動家計簿・資産管理サービス『マネーフォワード』 ■WebiPhone,iPadAndroid

ビジネス向けクラウドサービス『MFクラウドシリーズ』 ■会計ソフト『MFクラウド会計』確定申告ソフト『MFクラウド確定申告』請求書管理ソフト『MFクラウド請求書』給与計算ソフト『MFクラウド給与』経費精算ソフト『MFクラウド経費』入金消込ソフト『MFクラウド消込』マイナンバー管理ソフト『MFクラウドマイナンバー』資金調達サービス『MFクラウドファイナンス』

メディア ■くらしの経済メディア『MONEY PLUS』