Rails, RSpec, Spork(, Guard) で適切にクラスをリロード

もう少し簡潔に書けました。追記: Rails, RSpec, Spork(, Guard) で適切にクラスをリロード を参照。

Rails アプリケーションの開発時、spork と guard を使うと非常に効率がいいです。

この記事では spork 使用時にクラスを適切にリロードするための方法を紹介します (guard 関係ないけど、快適にテストできるとこまでのメモとして書いておきます)。spork と guard については、ググるか README みてください。

なお、ここではこの方法を確立した時点での最新リリースだった Rails 3.0.7 を想定してます。Rails 3.2 ではクラスリロードの方法が変わっているらしいのでそのままでは動かないかも。

spork / guard 使えるとこまで

ここはさらっと。

$ rails new blog --skip-test-unit
$ cd blog
# Gemfile
group :development, :test do
  gem "rspec-rails"
  gem "spork"
  gem "guard-rspec"
  gem "guard-spork"
  # gem "growl" # spork で spec の実行完了時に growl で通知してくれる。好みで。
end
$ bundle install
$ rails g rspec:install
$ spork --bootstrap
# spec/spec_helper.rb
# ENV["RAILS_ENV"] ||= 'test' 以下のコードを
# Spork.prefork do ... end のブロック内に移動
$ guard init spork
$ guard init rspec
# Guardfile
# spork で実行するよう :cli => "--drb" 追加
guard 'rspec', :version => 2, :cli => "--drb do

これで準備完了。

$ guard

で guard が起動するので、spec 下に適当な spec を書き、自動的に実行されることを確認する。

クラスのリロード

上記の手順を終えた状態では、クラスを書き換えても変更が反映されない。

クラスをリロードさせる方法は検索するといくつか見つかるが、うまくリロードできないケースがあったり、複数回クラスを load して、コールバック (before 何とかみたいなの) が重複して定義されるケースがあった。

いろいろ試した結果次の方法がよさそう。

# config/environment/test.rb
Blog::Application.configure do
  # Spork 使用時はクラスをキャッシュしない
  # これは development のデフォルト設定と同じ
  if defined?(Spork) && Spork.using_spork?
    config.cache_classes = false
  else
    config.cache_classes = true
  end
end
# spec/spec_helper.rb
Spork.each_run do
  if Spork.using_spork? # これがないと Spork 非使用時に失敗する
    # routes のリロード
    Blog::Application.reload_routes!
    # railties-3.0.7/lib/rails/application/bootstrap.rb よりコピペ
    # もともとは config.cache_classes が true のとき、リクエストごとに呼ばれる部分
    # ここでは spec が実行されるごとに呼んでいる
    ActiveSupport::DescendantsTracker.clear
    ActiveSupport::Dependencies.clear
    # オブザーバが動いてないので、有効にする
    ActiveRecord::Base.instantiate_observers
    # machinist gem を利用してる場合、blueprints を再度読み直す
    # load "#{File.dirname(__FILE__)}/support/blueprints.rb"
  end
end

基本的には development 環境でリクエストごとにクラスリロードしてるのと同じ方法なので、まともに動いてるようです。ただ、この方法だと controller のテストをする際にリクエストごとにクラスリロードしてしまって、遅くなるかも (未確認です)。

なお、上記の方法が本当に問題ないか不明なので、デプロイ前には spork も guard も使わない状態で全部テストすることをおすすめします。

おまけ

実際の開発時には Guardfile も一部オプションを変えてるので、一応紹介。

# Guardfile
# 起動に時間がかかるとあきらめちゃうことがあるので 30 秒までは待つよう wait: 30 追加
guard 'spork', rspec_env: { 'RAILS_ENV' => 'test' }, wait: 30 do ...
# 上でも書いた cli: "--drb" のほか...
# all_on_start: false - 起動時の spec 全部実行を無効に。時間かかるので。回帰テストは Jenkins さんにまかせます。
# all_after_pass: false - 実行した spec が全部パスした時の spec 全部実行を無効に。これも時間かかるので。
guard 'rspec', all_on_start: false, all_after_pass: false, version: 2, cli: "--drb" do ...

それではみなさま良き Rails ライフを。