刺身☆ブーメランのブログ

ヒーホー!もしかしてオイラはデッド?

2012-05-19

「あとで捨てることになるコードのテスト」について

同僚とこんな話をした。

例えば「キャンペーンサイトとプレゼント応募フォーム」のような、一時的にしか使わないことがわかっているコードに対するテストをどの程度書けば良いのか?納期は迫っていて、他にもっと優先度の高い仕事もあるとする。最低限 200 が返ってくることだけをテストすれば良いのか、 POST したらどのようなリソースが作られるかまで厳密にテストすべきなのか。

そのとき述べた僕の意見を書いてみる。

追記

同僚の名誉のために補足すると、その時は「不安な部分をテストすべき」という当たり前な結論に落ち着いたのだけど、そもそも詳しく聞いてみたら「僕ならここは不安だから書くと思う」と考える部分については、彼はすでに書き終えていて、その上でさらに厳密にテストを追加すべきだろうか?という問題意識を持っていた、という。なので、以下に意識高そうなことをつらつら書いているけど、同僚氏のほうが僕よりよっぽど意識が高かった、というわけです。

「テストの練習のために書く」という考え方

  • 一時的にしか使わない(あとで捨てることがわかっている)コードに対してもテストを書いたほうが良い(当然)
  • もろもろ優先度との兼ね合いで、最低限のテスト + pending で済ます、というのは現実的だ
  • 一方で、そういうコードはえてしてビジネスロジックもシンプルなので、テストが書きやすい場合が多い
  • であれば、テストを書く練習のためだと考えて、そのようなコードのテストもしっかり書く、というのもアリだと思う

「あとで消すのも最適化の結果消えるのも変わりはない」という考え方

  • コードもテストも、理解しやすさを損ねないならば、よりコンパクトなほうが良い
  • リファクタリングを経てコードやテストが非常にコンパクトになったり、本質的ではない部分をそぎ落としたら不要になって削除する、ということはありえる
  • 最適化を繰り返して極限までコンパクトにした結果、ゼロに収束した、ということ
  • アプリケーションの尺度で考えても同じことが言えて、リファクタリングを経たり仕様が変わったりした結果、あるコードとテストがごっそり不要になった、というのは不自然ではない
  • キャンペーンサイトのように期間限定で使うつもりで書くコードと、十分に最適化されていないまま書くコードは、たまたま不要だと意識するタイミングが違うだけで、結局はあまり変わりはない
  • よって、あとで捨てることになるコードのテストもしっかり書くという意識なり姿勢なり態度なりは、テスト信者的な価値観をわきに置いたとしても、支持されうるもものだと思う

みなさんはどう思いますか?

2012-05-12

MixIn に強い Webistrano Recipe の書き方

という技術記事を Qiita に投稿しました。 Webistrano を使っている方にオススメです。ぜひご覧ください。

MixIn に強い Webistrano Recipe の書き方 #webistrano #capistrano - Qiita

この記事は Kobito で書きました。 Markdown 記法で書いてリアルタイムプレビューできるのがとても便利です。こちらもオススメです。

Kobito - プログラマの技術情報記録に最適なMacアプリケーション

2012-05-11

Rake の task に対する spec の書き方

rake-confirm という gem を作ってもらったのでさっそく使ってみたところ、 db:rollbackdb:fixtures:load も production 環境でのうっかり実行を防ぎたかったので Pull Request を送った

テストのない Pull Request は reject されそうだなあと思ったので、 Rake の task はどうテストしたらいいのか調べてみたところ、

  • rake-confirm は Rake::Task#enhance を使って事前タスクというものを追加している
  • Rake::Task#prerequisites というメソッドは、タスクに追加された事前タスクのリストを返す

ということがわかった。それを踏まえて書いたテストがこれ https://github.com/hsbt/rake-confirm/commit/92b42a371335a614566d3e40ebc6ff35f91c7833

一般化して書くと、こうなる。

2012-04-28

オレオレ Git サブコマンドを作る

今日同僚と「git copy ってないの?」「ないすよ」という話をして、そういえば svn のころの癖でつい git help copy などと探してしまうし、別にあっても害はないかなと思って git-copy を作ってみた。

https://github.com/kyanny/git-copy

なんのことはない、単に cp(1) を実行するだけなんだけど、 Git は git-* という名前の実行ファイルを PATH の通ったところに置いておくと勝手にサブコマンドとして認識してくれるので*1、このように手軽にオレオレ Git サブコマンドを作ることができる。

git-copy は仕事の息抜きにちょろっと書いてみたジョークコマンドだけど、世の中には git-dailygit-now のような実用的なツールも存在する。もちろんサブコマンド化などせず独自の便利ツールを作ってもいいのだけど、慣習に従ってみるのも悪くない。 git foo bar のように実行できるとビルトインの機能と区別がつかないため、慣れるのもはやそうだ。複数行にわたる複雑な便利エイリアスを使っていたり、エイリアスが破綻したのでシェルスクリプトに切り出したりしている人は、いっそサブコマンド化してしまったらどうだろう。ついでに github か bitbucket あたりで公開してくれると喜ぶひとがいそう。

余談だが、なぜ git copy がないのかについてはSubversion と Git/Mercurial/Bazaar は全く別物 - tcha.orgを読むといいかもしれない。 Git はコンテンツ VCS だから、とあるが、どこかで「Git はファイルではなくコンテンツを追跡するので、ファイル名の変更などは内部でよしなに判断してくれるから、ファイル名変更の前後で『これらのファイルは同じ歴史を持つよ』とわざわざ教えてあげる必要はなく、したがって git copy は不要である」という話を読んだ覚えがある。入門Gitだったかな。

*1:http://labs.gree.jp/blog/2011/05/3528/ で知った。ドキュメントのどこに書いてあるかは探せなかった

2012-04-25

bundle のなかで bundle する

Bundler.with_clean_env と bundle install --gemfile について追記しました

bundle exec した環境下でさらに bundle exec したいことがある。 bundle exec rake resque:work で起動した Resque ワーカーのなかで system("bundle exec rake spec") のような外部コマンドを呼び出すとか。ありますよね。ぼくは最近ありました。そしてハマった (そしてググりづらかった) のでこれ以上犠牲者を増やさないためにブログに書く。

bundler は実行時にいくつかの環境変数を定義するが、この場合問題になるのは BUNDLE_GEMFILE と GEM_HOME だ。 BUNDLE_GEMFILE は bundler が参照する Gemfile のパスで、 GEM_HOME は gem のインストール先となるパスだ。

BUNDLE_GEMFILE は Bundler::Runtime#setup_environment のなかで設定される。ソースを追うと Bundler::SharedHelpers#default_gemfile を経由して Bundler::SharedHelpers#find_gemfile のなかで Gemfile のパスを探している。 find_gemfile はカレントディレクトリを起点として、 Gemfile というファイルが見つかるまで上位ディレクトリにさかのぼっていく。通常 bundle コマンドを実行するときは Gemfile が置いてあるディレクトリに移動しておくだろうから、 BUNDLE_GEMFILE にはカレントディレクトリの Gemfile のパスが設定されることになる。

しかし外部コマンドを呼び出したときは挙動が変わる。まず bundle install した gem に付属する実行ファイル (rake とか) は bundle exec 経由で呼び出せるように bundler 自身がラッパースクリプトで置き換える。このあたりの処理は Bundler::Installer のなかに書いてあり、実行ファイルのラッパースクリプトのひな形が lib/bundler/templates 以下にある。このひな形ファイルのなかで ENV['BUNDLE_GEMFILE'] が未定義だったら設定するようになっており、つまり BUNDLE_GEMFILE が定義済みであればその値をそのまま使う。

Ruby において Kernel.#system や Kernel.#exec などで外部コマンドを呼び出すと子プロセスで指定されたコマンドが実行されるが、親プロセスの環境変数を引き継ぐ。だから、 bundle exec で起動した親プロセスの中から system("bundle exec") という風に起動された子プロセスにおいては BUNDLE_GEMFILE 環境変数が定義済みで、この値は親プロセスの bundle exec を実行したときに参照した Gemfile のパスであるため、子プロセスで実行する bundle コマンドを別ディレクトリで実行しているつもりでもうまく動かない。

一方、 GEM_HOME のほうは gem のインストール先を指定する環境変数で、 bundler においては Bundler#configure_gem_home_and_path を経て Bundler#configure_gem_home というプライベートメソッドのなかで設定される。 configure_gem_home_and_path メソッドは ENV['GEM_HOME'] の有無をチェックして、未定義か disable_shared_gems オプションが指定された場合のみ configure_gem_home が呼び出される。

これも BUNDLE_GEMFILE と同様に、親プロセスで設定された値が子プロセスに引き継がれるため、例えば bundle exec したプロセスの中で、親プロセスの bundle exec を実行したのと別ディレクトリに移動し system("bundle install --deployment") を実行すると、期待に反して依存 gem が vendor/bundle 以下にインストールされない。

そういうディレクトリレイアウトの具体的な例は sorcery で、面白いことに spec/ ディレクトリ以下にバージョン別の Rails アプリケーションを丸ごと持っている。 rake spec すると各 Rails アプリケーションのディレクトリ内で rake spec を実行するのだが、これを Travis CI 上で行うと「bundle のなかで bundle 問題」が起こる。

Travis CI: Building a Ruby Project の Default Test Script や Dependency Management に記載があるように、 Travis CI は依存 gem のインストールとテスト実行に bundler を使う。おそらく bundle install --deployment && bundle exec rake spec のようなコマンドを実行しているのだろう。すると bundle exec rake spec のなかで bundle exec rake spec が実行されることになり、 sorcery の spec は各 Rails アプリケーションの Rails.root にあたるディレクトリ内にある Gemfile に基づいてテストが実行されることを期待しているのに、環境変数 GEM_HOME と BUNDLE_GEMFILE が悪さをしてテストがこける

sorcery にまつわる話は @banyan が教えてくれた。彼が送った Pull Request がマージされ、無事に Travis CI 上での sorcery のテストがパスするようになったようだ。

この問題は親プロセスの環境変数が子プロセスに引き継がれて、子プロセス側の bundler の動作に影響を及ぼすことが原因なので、環境変数をリセットすれば解決する。 Ruby 1.9 では外部コマンドの実行時に環境変数を上書きできるので、

env = {
  'BUNDLE_GEMFILE' => nil,
  'GEM_HOME' => nil,
}
system(env, "bundle install --deployment")

のようにして BUNDLE_GEMFILE と GEM_HOME の値に nil を渡して未定義にしてやれば、子プロセス側の bundler がよしなに環境変数を設定しなおしてくれる。

追記 Wed Apr 25 2012 22:46:41 GMT+0900 (JST)

ブクマコメントで教えてもらったが、 Bundler.with_clean_env というそのものズバリのメソッドが用意されていた。 bundler 1.1 から入った機能のようだ。このブログを書いてる最中に参照した bundle-exec(1) のマニュアルにも ENVIRONMENT MODIFICATIONS の一番最後の Shelling out の項に記載されていた。ソースを読むと BUNDLE_ ではじまる環境変数を削除してブロックを実行してくれるようだ。 clean_system, clean_exec なんてメソッドもある。 GEM_HOME をいじっている箇所は特定できなかった。 bundler/setup も require するので Bundler.setup -> Bundler::Runtime#setup も追ったがわからず。

それとは別に、 bundle install --gemfile オプションの存在もブログを書いてから知った。こちらも bundle-install(1) マニュアルに記載されている。こちらもソースを読むと、 --gemfile で指定したパスにファイルが存在すれば BUNDLE_GEMFILE を上書きしてくれる。こちらも GEM_HOME をどのように扱うのかはわからなかった。

Bundler.with_clean_env は当然 bundler を require していないと使えない。もしくは親プロセス自身が「自分は bundler の中で動いている」と知っている必要があるので、 require 'bundler' した上で常に bundle exec で実行するコードの中であれば、

Bundler.with_clean_env do
  system("bundle install --gemfile /path/to/rails/Gemfile")
  system("bundle exec rake spec")
end

# または
Bundler.clean_system("bundle install --gemfile /path/to/rails/Gemfile")
Bundler.clean_system("bundle exec rake spec")

のように呼び出すのがスマートなのかもしれない (繰り返すが GEM_HOME の扱いがどうなるかは追えていないし試してもいないのでこのコードが動く保証はない)

@banyan 調べでは、 sorcery の Rakefile には require 'bundler/setup' しているコードが "Too slow" とかいうコメントとともにコメントアウトされて残っているそうだ。おそらく bundle のなかで bundle とは別の理由で bundler を require していたがテストの実行に時間がかかり、外しても影響がないのでやめたままになっていてみんな忘れていたが、最近 Travis CI にのせてみたらテストがこけはじめた、けど開発者たちは $ rake spec のように bundle exec を介さず手元でテストを実行していたので何が問題かわからなかった・・・といったところだろうか。

(追記終わり)

思ったよりも長くなった上に途中から bundler のソースコードリーディングになってしまったけど、まとめると、外部コマンドの実行時におかしなことが起きたら環境変数を疑いましょう。それではみなさんすてきな bundle ライフを!