#285 Spork
- Download:
- source codeProject Files in Zip (94.6 KB)
- mp4Full Size H.264 Video (22.2 MB)
- m4vSmaller H.264 Video (11.5 MB)
- webmFull Size VP8 Video (13.7 MB)
- ogvFull Size Theora Video (27.8 MB)
食器の一種と同じ名前であるSporkは、アプリケーションのテストスイートの読み込みを高速化するRuby gemです。今回のエピソードではこのSporkを使ってアプリケーションのテストの速度を向上させる方法を紹介します。
通常Railsアプリケーションのテストスイートを実行すると、数秒間の停止状態のあとテストが開始されます。対象のアプリケーションのテストで、time
コマンドを使ってこの停止時間を計測してみます。
$ time rspec . ......... Finished in 1.98 seconds 9 examples, 0 failures real 0m11.090s user 0m9.735s sys 0m1.306s
RSpecの表示ではテストの実行時間はせいぜい1〜2秒となっていますが、rspec
を実行してからコマンドプロンプトに戻るまでの時間はそれよりもずっと長く、11秒を超えています。余分にかかっているのはRailsアプリケーションを読み込むための時間で、今見たようにこれはテスト自体を実行する時間よりも多くかかっています。Sporkはこの問題を解決するのに役立ちます。
Sporkのインストール
Sporkは他のgemと同じようにインストールできますが、テストのみで使用されるためGemfile
のtest
グループに追加します。Rails 3アプリケーション用には、このgemのプレリリース版(現状は0.9.0.rc
)をインストールします。
gem "rspec-rails", :group => [:test, :development] group :test do gem "factory_girl_rails" gem "capybara" gem "guard-rspec" gem "spork", "> 0.9.0.rc" end
いつもと同じく、Gemfile
を修正してbundle
を実行すると、記述されたgemがインストールされます。
次にSporkのbootstrap
コマンドを実行してヘルパーファイルを準備します。
$ spork --bootstrap Using RSpec Bootstrapping /Users/eifion/auth/spec/spec_helper.rb. Done. Edit /Users/eifion/auth/spec/spec_helper.rb now with your favorite text editor and follow the instructions.
上の出力からわかるように、RSpecを使用していることをSporkが検知してspec_helper
ファイルを修正しました。修正箇所を確認してみます。
require 'spork' Spork.prefork do # Loading more in this block will cause your tests to run faster. However, # if you change any configuration or code from libraries loaded here, you'll # need to restart spork for it take effect. end Spork.each_run do # This code will be run each time you run your specs. end
Sporkがspec_helper
に2つのメソッドを追加していて、それぞれがブロックを持っています。1つ目はprefork
で、Sporkサーバが起動するときに実行されます。もうひとつはeach_run
で、テストスイートを実行するたびに起動されます。spec_helper
のコードをできるかぎりprefork
に移して、なるべく一度実行されるだけで済むようにするのがいいでしょう。一度すべてのコードを移動してみて、それでもテストが正しく動作するか見てみます。
require 'rubygems' require 'spork' Spork.prefork do # This file is copied to spec/ when you run 'rails generate ↵ rspec:install' ENV["RAILS_ENV"] ||= 'test' require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' require 'capybara/rspec' # Requires supporting ruby files with custom matchers and ↵ macros, etc, # in spec/support/ and its subdirectories. Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| ↵ require f} RSpec.configure do |config| config.mock_with :rspec config.use_transactional_fixtures = true config.include(MailerMacros) config.before(:each) { reset_email } end end Spork.each_run do # This code will be run each time you run your specs. end
Sporkを利用するには、spork
コマンドを実行してサーバを起動します。
$ spork Using RSpec Preloading Rails environment Loading Spork.prefork block... Spork is ready and listening on 8989!
サーバが起動するとprefork
メソッド内のコードが実行されるので、このコードは一度だけ実行されるだけですみます。続いて別のターミナルウィンドウを開いて、テストスイートを実行します。テストはSporkサーバを介して実行しなくてはいけないので--drb
オプションを指定します。
テストをSporkを介して実行すると、specスイートはより早く実行されます。time
コマンドの下で再度実行してみると終了までの時間が短くなっています。これによって得られる時間は、Railsアプリケーションの規模が大きくなるほど増えていきます。
$ time rspec . --drb ......... Finished in 2.21 seconds 9 examples, 0 failures real 0m4.125s user 0m0.342s sys 0m0.097s
Sporkはとても便利なツールです。テスト環境でアプリケーションを一度起動して、テストを実行するたびにアプリケーションを読み込みなおすことなく、何回でも好きなだけ実行できます。
SporkとGuardの連携
Guardは、アプリケーションのテストスイートのコードが修正されるたびにテストを自動実行してくれるgemです。エピソード264[動画を見る, 読む]でその詳細を紹介しました。そのGuardをguard-spork
というgemを使ってSporkと連携させることができます。このgemはSporkのときと同じ方法でインストールできます。Gemfile
のtest
グループにgemを追加してbundle
を実行します。
gem "rspec-rails", :group => [:test, :development] group :test do gem "factory_girl_rails" gem "capybara" gem "guard-rspec" gem "spork", "> 0.9.0.rc" gem "guard-spork" end
gemがインストールされたら、guard init spork
を実行してGuardfile
にSporkを追加します。Guardfile
の中を見てみると、RSpecセクションの下にSporkセクションができています。
guard 'spork', :cucumber_env => { 'RAILS_ENV' => 'test' }, :rspec_env => { 'RAILS_ENV' => 'test' } do watch('config/application.rb') watch('config/environment.rb') watch(%r{^config/environments/.+\.rb$}) watch(%r{^config/initializers/.+\.rb$}) watch('spec/spec_helper.rb') end
この部分は他のテスト関連のguardよりも先に実行されなければいけないので、このセクションをRSpecセクションの上に移動します。さらにRSpec guard
を修正して、:cli
オプションを追加して値として--drb
を渡します。これによってspecがSporkを介して実行されるようになり、他の、例えばCucumber用のguardにも適用されます。
guard 'rspec', :version => 2, :cli => '--drb' do # Content omitted. end
これでSporkサーバを手動で起動する必要がなくなりました。Guardを開始すると自動的にSporkがバックグラウンドで起動され、アプリケーションのRubyファイルに変更が加えられるとテストスイートを実行します。
$ guard Please install rb-fsevent gem for Mac OSX FSEvents support Using polling (Please help us to support your system better than that.) Please install growl_notify or growl gem for Mac OS X notification support and add it to your Gemfile Guard is now watching at '/Users/eifion/auth' Starting Spork for RSpec Using RSpec Preloading Rails environment Loading Spork.prefork block... Spork is ready and listening on 8989! Spork server for RSpec successfully started Guard::RSpec is running, with RSpec 2! Running all specs Running tests with args ["--color", "--format", "progress", "--format", "Guard::RSpec::Formatter::NotificationRSpec", "--out", "/dev/null", "--require", "/Users/eifion/.rvm/gems/ruby-1.9.2-p180@rails31/gems/guard-rspec-0.4.5/lib/guard/rspec/formatters/notification_rspec.rb", "spec"]... ......... Finished in 4.29 seconds 9 examples, 0 failures Done.
これはSporkを別で管理するよりもずっと簡単です。アプリケーションのファイルかspecを変更して保存すると、関連のspecが1〜2秒後に実行されます。
Sporkの設定
次にSporkの設定を変更する方法を見てみます。アプリケーションのファイルを修正したときに、Sporkが変更を検知せず手動で再起動しなくてはいけない場合があります。この原因は、ファイルがprefork
ブロックで読み込まれ、each_run
では再読み込みされないからです。これはFactory Girlを使用している場合に発生するときがあります。その場合にファクトリはprefork
で読み込まれるので、テストスイートが再実行されるときにそこでの変更は検知されないからです。すべての変更が自動的に検知されるようにするためには、each_run
でファクトリを再読み込みする必要があります。Factory Girlの最新版にはこれを行うreloadメソッドがあるので簡単に対応できます。
Spork.each_run do FactoryGirl.reload end
ファクトリに対して行ったすべての変更は、次回のテスト実行時に自動的に検知されます。
同じような問題が/spec/support
ディレクトリ内のファイルでも起こる可能性があります。これらのファイルもprefork
ブロックで呼び出されるので、変更をしたとしてもテストが実行されるときに検知されません。ファイルを読み込むコードをeach_run
に移動させる方法もありますが、コードを移動させた分だけ、テストが実行される前の遅れの時間が長くなってしまいます。コードはpreload
ブロックの中に置いたままで、/spec/support
の中のファイルが変更されるたびに自動的にSporkが再読み込みされるようにできればその方がいいでしょう。
このためにはguard-spork
が役に立ちます。Guardfile
のspork
ブロックにはファイル名のパターンが含まれ、それによって監視するファイルが定義されています。パターンにマッチするファイルのいずれかが変更されるとSporkが読み込み直されます。ここにspec/support
ディレクトリを追加するだけです。
guard 'spork', :cucumber_env => { 'RAILS_ENV' => ↵ 'test' }, :rspec_env => { 'RAILS_ENV' => 'test' } do watch('config/application.rb') watch('config/environment.rb') watch(%r{^config/environments/.+\.rb$}) watch(%r{^config/initializers/.+\.rb$}) watch('spec/spec_helper.rb') watch(%r{^spec/support/.+\.rb$}) end
これでspec/support
ディレクトリ内のファイルへの変更がGuardに検知されてSporkが読み込み直されるようになります。
Guardfile
を見ているついでに、速度の遅いテストスイートに対応するための簡単なチップを一つ紹介します。実行時間が1分以上かかるようなテストはあまり何度も実行したくないでしょう。そのような場合にrspec guard
に渡す、役に立つ2つのオプションがあります。all_on_start
とall_over_pass
です。これらの両方をfalse
に設定します。
guard 'rspec', :version => 2, :cli => '--drb', ↵ :all_on_start => false, :all_after_pass => false do # watch commands omitted. end
これらのオプションが設定されると、Guardは前回失敗したspecが成功した場合にすべてのspecを再実行しません。これによって、すべてのspecが実行される場合の挙動を細かくコントロールできます。Guardターミナル(Guardの最新版を使用している場合)でリターンキーを押すとすべてのspecを実行します。
もうひとつのチップは、Sporkとは直接関連しませんが大きなテストスイートを扱う場合に役に立ちます。RSpecの設定ブロックに3行(以下に示すブロックの最後の3行)を追加します。
RSpec.configure do |config| config.mock_with :rspec config.use_transactional_fixtures = true config.include(MailerMacros) config.before(:each) { reset_email } config.treat_symbols_as_metadata_keys_with_true_values = true config.filter_run :focus => true config.run_all_when_everything_filtered = true end
これで、あるspecに:focus
タグを追加すると、そのspecだけが実行されるようになります。例えばUser
モデルをテストするspecに:focus
を追加します。
require 'spec_helper' describe User do describe "#send_password_reset" do let(:user) { Factory(:user) } it "generates a unique password_reset_token each time", ↵ :focus do user.send_password_reset last_token = user.password_reset_token user.send_password_reset user.password_reset_token.should_not eq(last_token) end # Other specs omitted. end end
上のファイルを保存すると、Guardは変更を検知しますが:focus
タグによってそのひとつのspecだけが実行されます。
Running: spec/models/user_spec.rb Running tests with args ... Run filtered including {:focus=>true} . Finished in 1.93 seconds 1 example, 0 failures Done.
意図に反してSporkのprefork
ブロックで一部のコードが呼び出される場合がありますが、それをeach_run
に移動するのは簡単ではありません。このようなケースに対処するためにSporkはtrap_method
メソッドを提供しています。このメソッドは、トラップされたメソッドがすぐに実行される代わりに、プロセスがフォークされた後に実行するようにします。これはMongoidやDeviseを使用する場合などに有効です。どちらも、ロードされてほしくないpreforkブロックの中身をロードするからです。これについてはSporkのwikiページでより詳しく説明されています。
Sporkについての今回のエピソードは以上です。Sporkはテスト駆動開発のプロセスを加速させる優れたツールで、上で紹介したチップスと組み合わせることで、大規模なRailsプロジェクトにも対応することができます。