@kyanny's blog

To be a cross-functional developer

ghn v2.0.0.pre5 released (final v2 pre release)

I'd like to make this as a final pre release of ghn version 2.

ChangeLog:

  • Add ghn.followissuecomment config and --follow-issuecomment command line option (see README)

Now "follow issuecomment anchor URL" feature is optional.


ghn v2.0.0.pre5 をリリースしました。これが最後の pre release になる予定です。

GitHub の Notification ページ に表示される issue や pull request へのリンクは、それが新着コメントのものだった場合 #issuecomment-XXXX という anchor つきの URL になります。 ghn もこの挙動と合わせて欲しいという要望をいただいたため 2.0.0 系の開発初期からその対応を組み込んでいましたが、自分で使っていてどうもしっくりこない気がしたので、デフォルトでは無効とし、オプションで有効にできるようにしました。

クライアントサイド JavaScript (AltJS) のテストを書くのは本当に難しいのか?

TL;DR - 最初の一人はつらいけど後続はそうでもないので先駆者は自覚と誇りを持ってオールグリーンを維持しよう

このエントリはMarionette.js ベースで3ヶ月開発したアプリのカバレッジ推移をまとめてみた - @kyanny's blogというエントリの続きにあたります。未読の方は先にそちらを一読されることをおすすめします。

Marionette.js ベースで3ヶ月開発したアプリのカバレッジ推移をまとめてみた - @kyanny's blogの結論で触れたように、今回テストを書くことにこだわったのは、「クライアントサイド JavaScript (AltJS) のテストを書くのは本当に難しいのか?」という問いに対する自分なりの回答を実践して検証してみたかったという理由があったからだ。

以前から「クライアント JavaScript (CoffeeScript や他の AltJS を含む) のテストを書くのは難しい、大変だ、割に合わない」などという意見を見聞きしてきたが、本当にそうなのか?という疑問があった。賛同できる部分もあるが、できない部分もある。感情的にも、ちょっと厳しい言い方をすれば、「テストを書かない理由を並べたてるのは甘え。つらくてもテストを書くのがイマドキの常識。お前それ t-wada さんの前でも言えんの?」みたいなことを思っていた。

そういう背景があり、今回クライアントサイドアプリケーションを「そこそこの」テストカバレッジを維持しつつ開発してきたわけだが、「テストを書くことの難しさ」にもいろいろあることがわかった。以下、経験を踏まえて得られた知見を紹介する。

Finding: チーム開発で得られた知見

件のアプリを開発していた3ヶ月間、自分でもコードもテストも書いてきたし、チームメイトのコードやテストをレビューもしてきた。それで気がついた、というか改めてわかったのは、「あるパターンのテストを最初に書くときが一番難しい」ということだ。逆にいえば、「あるパターンのテストケースがすでに存在すれば、それを真似たテストケースを増やすのは簡単」ということ。

Marionette/Backbone なアプリケーションなら、テストのパターンはおのずと決まってくる。 Model でむずかしいのはビューとのイベントのやりとりや Ajax 絡みのところだが、誰かが一度 Sinon.JS の Spy なり fakeServer なりを使ったテストケースを書けばあとはパラメータだけ書き換えて使い回すだけでいい。 View でむずかしいのも Spy の使い方や DOM イベント絡みのところだが、そこも誰かが上手に spy/mock/stub したテストケースを一つか二つ書けば、その後ほかの機能を開発する人はそれを真似つつドキュメントを読みつついろいろ試せばいいので、とっかかりやすい。

ふつうのウェブアプリケーション (Rails とか) と同じで、テストと機能開発をバランスよくコミットしていくためには、二つの壁がある。一つ目は、テストスイートを整備し、 CI に載せるところまで。これはわかりやすいタスクで、いろいろノウハウが必要なものの、新雪を踏みならすようなもので比較的楽しい作業だ(で、たいていの場合これをやるとそこで満足してしまい、次の壁を乗り越えられない)

二つ目の壁が、「正しく動く前例を作る」ということ。この壁はある程度開発が進んでからでないと顕在化しない。そもそもテスト対象がそれなりに複雑にならないと込み入ったテストを書く機会もない。この壁が出てくるのは開発の中初期、いわゆるプロトタイピングが終わって具体的な肉付けをし始める段階だ。このフェーズになると開発者は機能を作ることにフォーカスしているので、うまく通らないテストはフラストレーションのもとだし、開発スピードも鈍らせる。だからテストをないがしろにしがちだし、それが正当化もされやすい。

二つ目の壁は思っている以上に高く険しい。これに直面したとき、いかに諦めずテストを書いてグリーンを維持するかが極めて大事だ。ここで諦めてしまうと、あとはもうあっという間に落ちてリカバリは不可能になる。不確定なコードの上で動く不安定なアプリケーションを「直して祈る」憂鬱な日々が待っている(そして数年後まっさらから作り直されて、古いコードは捨てられる)

諦める、というのは開発者個人のマインドという意味でもそうだし、チームのマインドとしても同じことが言える。粘り強く、情熱あふれる個人が不退転の決意で正しい spy のやり方を見つけるというのもいいし、諦めて「ここテストするの難しいので pending でお願いします...」と弱気になったチームメイトを同僚が正しい道に導くというのもいい(具体的には「テスト書いてください。テスト無しではマージできません」と断固たる態度を示したり、かわりにテストを書いてあげたりする)

今回のプロジェクトでは、 Model だろうが View だろうが Controller だろうが、ほとんど常に機能のコードとテストコードはセットで Pull Request されていた。もちろん個々のケースをとってもカバレッジは 100% には届かないし、それらのテストがどの程度効果的なのか?についてはなんともいえない。

ただ、毎日 3~6 個ペースで新しい Pull Request ができ、毎回テストコードが含まれていたということは、このチームにとっては、クライアントサイドであってもテストを書くことがそこまで大きな負担にはならなかった、と言ってもいいはずだ。つまり、月並みな言い方だが、テストを書く気があれば書けないことはないのだ。

Conclusion: 結論

クライアントサイドアプリケーションのテストを書くうえで難しい部分があることは認める。しかし、それは必ずしもクライアントサイド特有の事情ではない。単に新しい概念やツールに習熟するための学習コストがかかっているだけ、という側面もあり、その点ではサーバーサイドも同様だ(RSpec でテストを書き始めたとき苦労しなかった人がいるだろうか?)

どのようなソフトウェアを書くかによっても難易度は大きく変わってくるだろうし、今回たまたまうまくいっただけかもしれない(所詮は個人の体験に基づいた話なので、一般化するほうに無理がある)けれども、テストを書く上で障害となりうる壁、特に開発が乗ってきた頃に直面する二つ目の壁の存在を認識していれば、立ち向かうことができるはずだ。

もしあなたの周りに、テストを書くのが難しいと不平をもらしているひとがいたら、少しの時間でいいから、何が難しいのか話を聞き、実際にコードを読んで難しさの理由を一緒に考えてあげてほしい。たぶんきっと、そのひとは本音ではテストを書きたくないわけじゃないのだ—ただ思うようにいかず嘆いているだけで、誰かがほんのちょっと背中を押してあげればまたいつものように元気よくコードディングに戻れるはずだ。


Photos taken by Nat W and Capt' Gorgeous

Marionette.js ベースで3ヶ月開発したアプリのカバレッジ推移をまとめてみた

6月の頭から3ヶ月ほどかけて、 Rails + 一部 jQuery 的なよくある構成のウェブアプリケーションのフロントエンド部分を Marionette.js をベースに作りなおした。メンバーはいわゆるウェブ系のスキルセットを持つ開発者3名。 Marionette.js の経験者は一人もいなかったが、別プロジェクトが先行して Marionette.js を採用して同様のリニューアルを終えたタイミングで、ある程度のノウハウと「なんとかなりそう」という手応えはあった。

せっかく新しくはじめるのだからちゃんとテストを書きたい、そしてカバレッジも計測したいと思い、先行していたプロジェクトで利用していた konacha ではなく、カバレッジ計測機能をもつ teaspoon を採用した。どちらも Rails アプリの assets に置かれる JavaScript/CoffeeScript に対するテストランナーで、 Asset Pipeline との連携をよしなにやってくれる。同様のツールに evergreen がある。

で、そろそろ開発も終盤、大詰めという状況で、これまで計測してきたカバレッジデータを集計し、推移をグラフ化してみた。なお、計測対象となるアプリのコードは全て CoffeeScript で書いているが、カバレッジはコンパイル後の JavaScript に対する数字になる点は留意する必要がある。

f:id:a666666:20140830015932p:plain

f:id:a666666:20140830015924p:plain

f:id:a666666:20140830015944p:plain

f:id:a666666:20140830015940p:plain

f:id:a666666:20140830015954p:plain

f:id:a666666:20140830015949p:plain

f:id:a666666:20140830020002p:plain

f:id:a666666:20140830015958p:plain

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 でカバレッジ計測用に別途ビルドするなり、やりようはあったと思う。

Git でコミットの親子関係を master からの相対距離で調べる方法

あるいは、ある二つのコミットが、

  1. どちらも master ブランチに含まれているか
  2. どちらのコミットのほうが先に master ブランチにマージされたか

を調べる方法。

git name-rev --refs master を使った。

$ git log --oneline -1
5d42715 Merge pull request #1048 from quipper/awesome-branch
$ bundle show schema
/Users/kyanny/.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/bundler/gems/schema-45569067e8bb

$ git log --oneline -1
ab2f0fe Merge pull request #1129 from quipper/yet-another-awesome-branch
$ git name-rev --refs master 5077d26194181521de8a1ccc0f02782416795452 # A
5077d26194181521de8a1ccc0f02782416795452 master~5^2
$ git name-rev --refs master 38883c52ad5c1657b89963e48c505c9f9f92574e # B
38883c52ad5c1657b89963e48c505c9f9f92574e master~5 # C
$ git name-rev --refs master 45569067e8bb
45569067e8bb master~1

解説

ある Rails アプリが schema という別の gem に依存しており、 schema 側の変更を取り込むために任意のタイミングで bundle update している。

自分が取り込みたい変更は 5077d26194181521de8a1ccc0f02782416795452 のコミット(以下 A)で、 schema リポジトリではすでに master にマージされている。38883c52ad5c1657b89963e48c505c9f9f92574e がマージコミット(以下 B)。

ところが別の作業で schema のリビジョンが更新されており、現在 Rails プロジェクトの master HEAD では schema の 45569067e8bb を使っている(以下 C)。

ここで、 A も C も master に含まれていることはわかっているが、どっちが先祖にあたるのかを知りたい。

図で示すと、↓が期待する歴史

---A---C---master

↓のようになっていると、 A はまだ Rails アプリ側には取り込まれていないということなので、困る。

---C---A---master

これを、 git show でタイムスタンプを調べたりするのではなく、もっと明らかな形で確かめたかった(タイムスタンプは前後することがあってあてにならないし、 GitHub Issues 等でテキストベースでやり取りしているとき結果をコピペして説明しづらい)

そこで冒頭の一連のコマンドを使った。 --refs master をつけないと remotes/origin/ultra-super-awesome-branch~3^2 のような結果が出てきてしまい、わかりづらかった。

冒頭のコマンドの実行結果でいうと、

  • A のマージコミットが B なので ---A---B---master が常に成り立つ
  • A は master~5^2 なので master からみて少なくとも 5 つ前のコミットにあたる
  • C は master~1 なので master からみて 1 つ前のコミットにあたる

以上から、 ---A---B---C---master が成り立ち、コミット A は無事 Rails アプリに取り込まれていることがわかった。

参考

ghn v2.0.0.pre4 released

http://rubygems.org/gems/ghn/versions/2.0.0.pre4

ChangeLog:

  • Support open(1) alternative commands at some platforms

Thank you @syohex san to contribute patch!


ghn v2.0.0.pre4 をリリースしました。 Mac OSX 以外のプラットフォームでは open(1) コマンドの変わりに相当する別コマンドを利用するようになりました。 @syohex さん Pull Request ありがとうございました。