6月の頭から3ヶ月ほどかけて、 Rails + 一部 jQuery 的なよくある構成のウェブアプリケーションのフロントエンド部分を Marionette.js をベースに作りなおした。メンバーはいわゆるウェブ系のスキルセットを持つ開発者3名。 Marionette.js の経験者は一人もいなかったが、別プロジェクトが先行して Marionette.js を採用して同様のリニューアルを終えたタイミングで、ある程度のノウハウと「なんとかなりそう」という手応えはあった。
せっかく新しくはじめるのだからちゃんとテストを書きたい、そしてカバレッジも計測したいと思い、先行していたプロジェクトで利用していた konacha ではなく、カバレッジ計測機能をもつ teaspoon を採用した。どちらも Rails アプリの assets に置かれる JavaScript/CoffeeScript に対するテストランナーで、 Asset Pipeline との連携をよしなにやってくれる。同様のツールに evergreen がある。
で、そろそろ開発も終盤、大詰めという状況で、これまで計測してきたカバレッジデータを集計し、推移をグラフ化してみた。なお、計測対象となるアプリのコードは全て CoffeeScript で書いているが、カバレッジはコンパイル後の JavaScript に対する数字になる点は留意する必要がある。
raw data はこちら。 https://gist.github.com/kyanny/4447073eda7eb18ecbec
Method: 集計方法
CircleCI の REST API を利用して対象プロジェクトの master ブランチのビルドをたどり、ログの中から teaspoon の text summary 形式のカバレッジデータを抜き出して整形した。具体的には collect_data.rb を利用した。その後 plot_graph.rb を利用して gnuplot でデータをグラフ化した。
なお、後述する計測方法のミスのため、最後のビルドのカバレッジのみ master ではないブランチのものを使っている。
Finding: 得られた知見
まず真っ先に目を引いたのが、どの種類のカバレッジもビルド番号 1500 番ごろを境にがくんと悪化している点だ。この作業を始めたとき、 master ブランチの最新ビルドのカバレッジの数値を見てみたら、このような結果だった。
Statements : 44.84% ( 11958/26670 ) Branches : 21.13% ( 3211/15200 ) Functions : 47.76% ( 2582/5406 ) Lines : 44.63% ( 10738/24062 )
ステートメントカバレッジが約 45% というのは、「実際にコードを書いたりレビューをしたりしてた体感と比べるとちょっと低い、けどそこまで悪くもないかな?」というのが正直な印象だった。
一方、今回計測対象とした最古のビルドのカバレッジは以下のとおりだった。
Statements : 72.59% ( 739/1018 ) Branches : 25.91% ( 71/274 ) Functions : 70.77% ( 230/325 ) Lines : 66.45% ( 509/766 )
ステートメントカバレッジは 72% だったので 25% 以上も下がっているが、両端のデータを見ただけでは「だんだんテストが後回しになっていって徐々にカバレッジが下がっちゃったのかな」としか思わなかった。
ところがグラフで見てみると明らかにそういう自然な推移ではない。開発中盤で基礎となるパーツが揃い、一気に実装のペースがあがったとしても、ほぼ垂直にカバレッジが下がるのはおかしい。そこでその周辺のビルドとそれに関連する Pull Request を調査したところ、外部プロジェクトとして開発していた内製ライブラリとその依存ライブラリ群をインポートしていた。本来カバレッジの計測対象外とすべきファイルが除外されなかったためにカバレッジが落ちたのだ。つまり設定漏れによる計測ミスだった。
それに気づいて該当ファイルを計測対象から除外し、改めて計測し直したのがグラフの右端のデータで、カバレッジが急激に改善したことがわかる。これはグラフにして見てみなければ気づけなかった。なおその実データは以下。
Statements : 67.59% ( 4872/7208 ) Branches : 29.08% ( 650/2235 ) Functions : 64.44% ( 1484/2303 ) Lines : 64.5% ( 3950/6124 )
ステートメントカバレッジは最古のビルドと比較して下がっているものの、ブランチカバレッジは逆に改善している。全体の傾向として、プロジェクトの初期から終盤までほぼ同程度のカバレッジを維持できていたと言っていいだろう(上記の計測ミスにより中盤以降の実データは本来よりも悪い数字になってしまっているため、あくまで仮説だが)
次に気になったのは他の指標と比べたときのブランチカバレッジの低さだ。ブランチカバレッジはステートメントカバレッジに比べて厳しい基準だとはいえ、 30% を切っているのは見過ごせない。これはおそらく、いわゆる正常系、かつ成功ケースの分岐に対するテストしか書いていないからではないかと考えている。
また、これはチームのコーディングスタイル嗜好によるものだが、関数を細かく分けて実装しているクラスが多い。こんな風に。
class AwesomeController extends Marionette.Controller initialize: (opt) -> entity = App.request "entity" opt.entity_id @handleEntity entity handleEntity: (entity) -> @layoutView = @getLayoutView() @listenTo @layoutView, "show", => @entityRegion entity App.mainRegion.show @layoutView getLayoutView: -> new AwesomeLayout entityRegion: (entity) -> entityView = new EntityView entity: entity @layoutView.entityRegion.show entityView
ファンクションカバレッジがそこまで低くないのは、このように単一の機能しか持たず副作用も少ない、テストしやすい関数が多いことも一因かもしれない(うれしい予想ではないが)
実際、自分の記憶に照らし合わせてみても、単純な関数に対するテストはしっかり書く一方で、込み入った処理の流れがたくさん書いてある大きな関数に対するテストはあまり網羅的には書けなかった記憶がある(とりあえず関数を一回実行してみる、という程度)
うれしい発見もあった。グラフによれば、どのカバレッジのパーセンテージも中盤でがくんと落ちたものの、それ以後はゆるやかな右肩上がりを維持している。開発が佳境に近づいてもチームはテストを書くことをやめなかった、と言える。テストを書くことは開発のスピードを阻害する要因にならなかった、とも言えるかもしれない。
Conclusion: 結論
CoffeeScript で Marionette.js をベースに3ヶ月間3人チームで開発してきたクライアントサイドアプリケーション(Rails アプリのフロントエンド)のコードカバレッジの推移と、そのデータから得られた知見をまとめた。
このエントリは「クライアントサイド JavaScript (AltJS) のテストを書くのは本当に難しいのか?」という問いに対する自分なりの回答を示すものとして書き始めたが、いざ根拠となるデータを分析しはじめたらそれだけでけっこうなボリュームになってしまった。そこで、このエントリはここで一旦終わりとする。
「クライアントサイド JavaScript (AltJS) のテストを書くのは本当に難しいのか?」という問いについては、以前から思うところがあり、今回のプロジェクトでテストを書いたりカバレッジを計測したりしたのもそれなりに強い思い入れがあって実施したことだった。なので、その背景や、実際に一つのプロジェクトを経験してみての自分の意見などについては、別エントリで改めて触れたい。
追記
計測ミスについて。これはそもそも「カバレッジが急激に悪化したタイミングで異常を検知できなかった」というのが根本的な問題で、なぜ検知できなかったかというとカバレッジデータを継続してモニタリングできるようにしていなかったから。
なぜモニタリングできてなかったかというと、この Rails アプリは Code Climate でコードの品質チェックやセキュリティチェックを行っており、ついでにカバレッジも送っていた。しかし Code Climate は Ruby のカバレッジ計測データにしか対応していないので、 Marionette.js アプリ部分のカバレッジのレポーティングについては別途考える必要があった。結局ログをだしておくにとどまり、ツケを払うはめになった、という次第。
便利な外部サービスを積極的に利用していると便利さに依存しがちで、提供される便利さの枠内で物事を考え、できないことは諦めてしまいがちなのは良くない傾向だ。この部分だけ Coveralls と連携させるなり、 Jenkins でカバレッジ計測用に別途ビルドするなり、やりようはあったと思う。