有頂天Ruby

ビールを飲みながらRubyについて書きます。

私がMinitestとRailsのFixturesにハマった7つの理由

Brandon Hilkertの7 reasons why I'm sticking with Minitest and Fixtures in Railsの雑な日本語訳です。

誤訳が雑すぎる訳やあれば、Twitter@nilp_までお声かけいただくか、コメントやブコメで指摘してもらえると幸いです。

私がMinitestとRailsのFixturesにハマった7つの理由

私は先月グリーンフィールドのRails 4.1アプリの唯一の開発者として過ごす機会に恵まれました(訳注: なんの制約もないことをGreenfieldと言うそう)。 既存のコードをメンテするのにかなりの時間を費やしたことのある人にとって、 パターンを設け、ツールを選択する自由があることは非常に喜ばしい変化です。 私が行った選択の1つがMinitestRailsのfixturesを使うことです。

端的にいうと、それは最高でした! どんだけヤバかったかというと私が将来それ以外のものを使うことが想像出来ないぐらいです。

背景

私は2009年にRailsをはじめました。 当時、他に誰も使っていないように感じましたが、Railsのコアチームはtest_unitを使っていました。

私はそれが真実ではないと知るまで、Rspecの方がテスティングフレームワークとして圧倒的な人気を誇ってるようにみえていました。

Ruby/Railsを覚えるまでテストの経験を持たなかった身として、 私はどうやってテストするかそしてなにをテストするかを学ぶための情報を探していました。

当時(...そして今もなお)、Rubyのテスティングテクニック、 とりわけRailsアプリケーションの何をどうやってテストするかにフォーカスを当てた本はあまり多くありません。

なので、私がThe Rspec Bookに出会ったとき、 私はRubyでテストを書くための概念とベストプラクティスを理解するのに役立ちそうな格式ばった本にようやく出会えたことに興奮しました。 それにくわえて、ほとんどのRailsチュートリアルRSpecをインストールするところからスタートするようにみえました。 私は何をどうすればいいか全くわからなかったので、流れに従いRSpecを私のテスティングツールボックスの中心に据えました。

私は、考えてみれば、Test::UnitやMinitestに公平な機会を与えなかったことを認めます。 わたしはすぐにRSpecの深みにはまりました、理由があるわけでもなく、最近まで他の選択肢について考えることもありませんでした。 幸いなことに、最近ではより多くの人がMinitestを知っており、取っ掛かりにふさわしい方法だと考えているように見えます。

私の以前の設定

私の前のプロジェクトの典型的なGemfileはこんな感じです:

group :test do
  gem "rspec-rails"
  gem "factory_girl_rails"
  gem "capybara"
  gem "selenium-webdriver"
  gem "database_cleaner"
  gem "shoulda-matchers"
end

ほかのgemはさておき、私のspec_helper.rbトランザクションをオフにするなど(database_cleanerのため)Railsの標準的なテスト規約に沿わないいくつもの設定がありました。

この設定はメチャ複雑で、複雑さを管理するためのgemがあるくらいです。 フレームワークのために数えきれないほど多くの規約があって、私にとってそれはまともには思えませんでした。

以下は私が1ヶ月をMinitestとfixturesをRails 4.1のアプリケーションに費やした結果得られた知見です(順番に特に意味は無いです):

1. Fixturesは "本物" のデータに対してテストするよう強制します

フィクスチャデータは現実のものではないです。 それは準備されたものであなたは何をどれぐらい加えるか管理することができます。

私は家族向けのテレビ番組や映画のように共通の対象を中心に据えることで、 あなたが既に関係するキャラクターのヒエラルキーを思い描いているように、 アプリケーションの中身へと素早く入り込むことが出来ることに気づきました。

以下は私のusers.ymlフィクスチャから抜き出したサンプルです:

fred:
  first_name: Fred
  last_name: Flintstone
  email: fred@flintstone.com
  title: CEO
  password_digest: <%= ActiveRecord::FixtureSet.default_password_digest %>
  company: flintstone
  confirmed_at: <%= Chronic.parse("1/1/2014") %>
  invitation_token: <%= SecureRandom.urlsafe_base64 %>

wilma:
  first_name: Wilma
  last_name: Flintstone
  email: wilma@flintstone.com
  title: COO
  password_digest: <%= ActiveRecord::FixtureSet.default_password_digest %>
  company: flintstone
  supervisor: fred
  invitation_token: <%= SecureRandom.urlsafe_base64 %>

興味深いことに、昔はアソシエーションにおいてつらみがあったかもしれませんが、 flintstoneをcompanyの名前として使えて、そしてそれが同じ名前のcompanyフィクスチャを参照するとしたらどうでしょうか:

flintstone:
  name: Flintstone Inc.
  phone: 888-555-1212
  updated_at: <%= 6.months.ago %>
  created_at: <%= 6.months.ago %>

アプリケーションの全て時点において、私にはテストのためによく構造化されたデータがあります。 Factory Girlを使うと、もしあなたがアソシエーションとシードデータを持つような複雑なデータ構造をつくろうとした場合、イライラさせられます。

ActiveRecordのアソシエーションが複雑になったとき、 私はそれぞれのテストでセットアップを行うせいでイライラさせられること、 そして初期データを作るためにかかる時間が増えていくことに気づきました。

これを回避するため、既に設定されたアソシエーションと共にfactoryを使う方法がありますが、私はそれがすぐにあなたをstubに推し進めてしまうと思っています。 私はstubしてる場所が実装ベッタリなとき、たとえqueryが同じデータを返すとしても、検索条件を1つ変えるだけでテストが落ちてしまうのを直に見てきました。

最近のTDDについてのディスカッション では似たような話題についてたくさんの議論を呼びました。

総じていうと、私はmockとstubが少ないほど、私の書いたテストに自信を持つことができます、 そしてfixturesを使ったとき私はしばしばmockとstubは必要ないと感じました。 私のデータは想定の範囲内です、だから私は私のテストに自信を持てます。

最後になりますが、それぞれのテストの前にFactory Girlがデータを挿入するとき、 そこにはデータベースとやりとりするためのコストがかかります。 そのコストはそれを何度も行うことで増えていき、あなたのテストスイートは遅くなります。 Fixturesはテストスイートが実行される前に挿入されます、なのでテストに固有の変更がないかぎり、 概ね追加の挿入が必要になることはありません。 これによりもたらされる利益は最初は微々たるものですが、その利益は時間が経つにふえ、あなたの手元には よりパフォーマンスがよく、より頻繁に実行されやすいテストスイートが残るでしょう。

注: 私はbuild_modelのような、データベースに触れずARオブジェクトを作るメソッドがあることについては知っていますが、テストではデータベースのデータを使う必要がある場合(例えば、スコープや、状態が変化するメソッドなどなど)があります。

言うまでもないと思いますが、あなたは簡単にfixturesのデータをdevelopment環境にロードして使えます。

2. RSpecは何かをするために複数の方法を提供します

これは例えば2つのものが同じかどうかをどうassertするかといった単純なトピックでさえ、混乱を引き起します。 あなたは、そして私はeq()==かあるいはeql()のどれを使えばいいのでしょうか? 誰も分かんなくない???

"?"で終わるメソッド周りのファンシーな記法についてはどうでしょう?

post.should be_active

ちょっとまった、そう、どこにbe_activeメソッドがあるのでしょう? いいやっ! テストにおいてアプリケーションに本当にちゃんとactive?があるか確かめるために、RSpecメソッドをパースしています。

最初は、その魔法に魅了されるでしょう。 しかし、そのあとで、単にactive?とタイピングしてそれをtruefalseとアサートするべきだときづいたとき、 私は何をどうやって書くかに悩まされすぎていることに気づきました(もちろんまったく違う方法でこれを書くことも出来るでしょう)。

post.active?.should be_true

...あるいはこう:

post.active?.should == true

これで十分かって?

私はシンプルなことの良さと、一般的にassertionを書く方法はただ1つしか無いことを学びました:

assert post.active?

正しいassertionの書き方についてこれ以上ぐだぐだするのはさておいて、私はこの方法がテスト中のシンタックスエラーを少なくすることに気づきました。 アプリケーションコードをテスト出来るまでに、2回も3回もテストのエラーが出るのはイライラするし時間の無駄です。 同じことをやるときに限られた方法を使うことは、私がテストを書いているときにシンタックスエラーを起こすことを少なくしてくれました。

3. Capybaraのセットアップは簡単です

あなたが過去にカピバラを使用したことがあるなら、 あなたはそれをFactoryGirlと合わせて使うのは...奥が深いことを知ってるでしょう。

database_cleanerの設定に関するAvdiの記事 を私も設定しました:

RSpec.configure do |config|

  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
  end

  config.before(:each) do
    DatabaseCleaner.strategy = :transaction
  end

  config.before(:each, :js => true) do
    DatabaseCleaner.strategy = :truncation
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end

end

もちろん、この設定は、まずspec_helper.rbでtransactionを無効にしたあとやります:

config.use_transactional_fixtures = false

なぜ、このすべての設定が必要なのか?

あなたがFactory Girlを使用し、テスト中にデータベースにデータを挿入する場合、それはトランザクション内で行われます。 テストが終わったとき、トランザクションロールバックされデータは永続化されずに白紙の状態で次のテストがはじまります。 これはうまく正しく動くように見えるでしょう? でも全てがうまくいくわけではないのです...

もしあなたがseleniumのようなJavaScriptが有効なドライバーをインテグレーションテストを実行するために使う場合、 ブラウザの操作は違うスレッドで実行されます。

これでは他のスレッド/トランザクション中で設定されたデータを見ることはできません。

以上の理由により、あなたはtruncation strategyを頼らざるをえないのです。 それゆえ、以下のような設定になります:

config.before(:each, js: true) do
  DatabaseCleaner.strategy = :truncation
end

これは複雑で初心者にはわけがわかりません。 更に、あなたがマルチスレッドのコードあるいはgemを使っている場合、話は余計にややこしくなります。

あなたがかわりにfixtureを使うことにした場合、データはそれぞれのテストの開始時に挿入され(独立したトランザクション中ではない場合)、 そのデータは引き続き実行されるどのスレッドでも、ブラウザの操作でも、どこでも利用可能です。

factoryの代わりにfixturesを使う場合、通常の場合はdatabase_cleanerを使う必要がなくなります。 それに加え、Capybaraを使うために必要な設定はtest_helper.rbに以下の内容を追加するだけです:

require "capybara/rails"

class ActionDispatch::IntegrationTest
  include Capybara::DSL
end

これだけなのです...マジで...私がヤバイと思うのは、この設定はRailsのテストスイートでCapybaraとRSpecを使う場合、非常にありふれた設定だということです。

私達はRailsが、我々がぶち当たるような一般的な問題のためのソリューションを提供してくれていることに甘んじていました。

truncating vs transactionsを決めることや、database cleanerの複雑な設定を行っていることは、理にかなっておらずRails体験に反しているようにみえます。

私は率直にいって、長い間これが理にかなっているように見えていたことに驚いています。

FactoryGirlのような慣習/ツールはコミュニティーを助けるというより逆に傷つけてしまっている可能性はないでしょうか?

4. 複雑なスタブ/モックをしないことがコードをシンプルにする

RSpecはいつでも/どこでも簡単にstubをすることができます(上の方の#1みたいに)。 これにはいくつかの利点がありますが、簡単に腐った使い方が出来てしまいます。 実際、私はfixturesを使うことで、それほど頻繁にstubを使わなくなりました。 顧客がアプリケーションを訪れていい体験をしたとき、顧客はそのデータがスタブされているかどうかなんて全く気にしてません。 私のテストがデータとアプリケーションに対して動いていているという事実を毎日見ることは、 新しいコードがプロダクションに行ってもうまく動くという自信を私に与えてくれました。

Minitestは読みやすく使いやすいモッキングライブラリをもっています。 それはRSpecが最初からもってるほど多くの機能があるわけではないですが、 しかし、あなたがより多くの機能をもとめるなら、 mocha gemやそれと同じようなものを使えます(私は必要なかったです)。

5. Minitestのassertionの順番がわからないときスニペットが助けてくれる

私が最初にMinitestにとりかかったとき、私の頭のなかには長い間「どちらが期待する値でどちらが実際に得られた値なのか」という疑問が浮かんだままでした。 今は、私は補助がなくてもそれについて熟知しています、しかし、必要もないのにめっちゃタイプすることもないでしょう。

私は引数の順番を覚えておく苦痛を取り除いてくれるVimのためのRubyスニペットをすごい頼ってます。

f:id:nilp:20140601165400g:plain

6. MinitestはただのRuby

公平を期すためにいうと、RSpecも同じくただのRubyです。 しかし一般的にいえばRSpecは全てにおいて―shared examplesや、テストのセットアップや、設定など―魔法のような方法に見えます。

これらは全て "RSpec way" を持っているようにみえます。

Minitestでは単にRubyを使ってこれを扱います。もしあなたがshared examplesが必要なら、 共有したいテストを含んだモジュールをインクルードすればいいですよね?

私は最初の数日間のMinitestの経験で、何かをするための "正しい" 方法を探しているのに気づきました。 私は、尊敬しているコミュニティの人たちに諭され、それがただのRubyなんだと実感しました。 その考え方は私に、RSpecDSLによる魔法を使うというより、その言語を使ってやりたいこと全てをやっていいんだと気づかせてくれました。

ある意味では、たくさんの魔法は私達の視野を狭めてしまうと思います。 いくつかの素敵なものを使って(悪い使い方もして)、我々は、自分たちが使っているツールが、我々の全ての問題を解決してくれるんだと信じはじめていました。 Minitestを使うことがこの考え方を打ち砕き、私が培ったRubyのスキルに依って私がテストする上での課題を解決出来るということを認識させてくれました。

7. Railsのデフォルトから脱線することが、いつも価値を提供するわけではない

もちろん、Railsの全てが理想通りとではないことはわかります。 ですが、強い信念によって作られたもののによってどれだけ多くの人が救われているかを考えてみると、それは賞賛に値します。 先月、Railsのデフォルトスタックに頼るようになってから、私は私がツールを選ぶときにどれだけシンプルかを考慮していなかったことを実感しました。

私はコミュニティがRSpecとFactory Girlを使っていることが多いこと(私にはそう見えていた)をツールを選ぶ基準にしていましたが、それはいい考えでした。

一方で、シンプルさとコミュニティの大きさ両方の面で考えることができたなら、私はRSpecを使うことはなかったでしょう、 私はそれがいいものだと思っていましたが、それは単に他のものを知らなかっただけです。

RailsのレールにのってデフォルトスタックのMinitestとFixturesを使うことの手軽さは私の考えを変えました。 そこには最小限の設定があって、ほとんどの必要なことは非常に小さな追加の設定をするだけですみました。

私はまだ他の人が話してるようにfixtureを使うつらみを感じたことはありません。 しかし私は前もって考えもせずにデータに大きな変更をもたらさないよう気をつけてきました。

たぶん、このアプリがまだ十分に大きくなっていないから? 私のデータが十分複雑になっていないから? あるいは私がデータの変更がもたらす影響に十分注意しているからなのか?

それがなんであれ、今のところ私にとってそれは機能しています。 「 うおおおお! これがみんながRSpecとFactory Girlを使う理由か。 」というときが来るかもしれないと感じることもありますが。 もっとも、今の時点では想像もつきません。 デフォルトスタックは私にとって機能してるし私が好きなんだから、そんなことはどうでもいいんだよ。

まとめ

私はあるフレームワークと他のフレームワークのパフォーマンス合戦について言及するのを意図的に避けました。 しかし、このトピックについてこのspeakerdeckは素晴らしいベンチマークを提供しています

私は私のツールキットにMinitestとfixtureを加えることが出来てとても興奮しています。 私がこれまでのところ見てきた利益の前では、私が将来これ以外の別のものを選択し使うことは想像できません。

もしあなたがデフォルトのMinitestのアサーション記法に不満なら、 あなたはMinitestがspecスタイルの書き方をオプションで使えることを知って嬉しくなるでしょう。 もちろんそれはRSpecシンタックスと全く同じものではありませんが、それはより自然言語っぽい書き方に近づけてくれるでしょう、もしもあなたにとって自然言語っぽさが大事ならね。