Spork+Guardによる高速自動テスト

概要

テストの実行を高速化するSporkと、自動化するGuardを組み合わせて、快適なテスト駆動開発(TDD)環境を作ります。



構成

Ruby 1.9.2p180 (2011-02-18 revision 30909) [x86_64-darwin10.7.0]
Rails 3.1.0
RSpec 2.6.0
Spork 0.8.5
Guard 0.7.0
guard-spork 0.2.1
guard-rspec 0.4.5



gemパッケージのインストール

以下のgemパッケージをインストールします。

Gemfileはこんな感じになっています。

group :development, :test do
  gem 'rails3-generators'
  gem 'sqlite3-ruby', :require => 'sqlite3'
  gem 'ruby-debug19', :require => 'ruby-debug'
  gem 'rspec'
  gem 'rspec-rails',  '>= 2.0.0'
  gem 'spork',        '>= 0.8.5'
  gem 'guard'
  gem 'guard-spork'
  gem 'guard-rspec'
  gem 'rb-fsevent'
  gem 'growl'
  gem 'factory_girl'
  gem 'annotate',     :git => 'git://github.com/ctran/annotate_models.git'
end

gemパッケージをインストールします。

$ bundle install



Sporkの設定

次のコマンドを実行すると、spec_helper.rbの先頭にSpork.preforkとSpork.each_runというメソッド呼び出しが追加されます。

$ bundle exec spork --bootstrap
Using RSpec
Bootstrapping /Users/tetsuyai/dev/projects/myapp/spec/spec_helper.rb.
Done. Edit /Users/tetsuyai/dev/projects/myapp/spec/spec_helper.rb now with your favorite text editor and follow the instructions.

Spork.preforkに渡したブロックはサーバー起動時に実行され、Spork.each_runに渡したブロックはテストを実行するたびに実行されます。

FILE: $RAILS_ROOT/spec/spec_helper.rb

@@ -1,3 +1,32 @@
+require 'rubygems'
+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
+
+# --- Instructions ---
+# - Sort through your spec_helper file. Place as much environment loading
+#   code that you don't normally modify during development in the
+#   Spork.prefork block.
+# - Place the rest under Spork.each_run block
+# - Any code that is left outside of the blocks will be ran during preforking
+#   and during each_run!
+# - These instructions should self-destruct in 10 seconds.  If they don't,
+#   feel free to delete them.
+#
+
+
+
+
 # This file is copied to spec/ when you run 'rails generate rspec:install'
 ENV["RAILS_ENV"] ||= 'test'
 require File.expand_path("../../config/environment", __FILE__)

spec_helper.rbを開いて、もともとspec_helper.rbに定義されていた処理をすべて、Spork.preforkのブロックの中に移動します。

+require 'rubygems'
+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.
+
   # 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'
 
   # 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|
     # == Mock Framework
     #
     # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
     #
     # config.mock_with :mocha
     # config.mock_with :flexmock
     # config.mock_with :rr
     config.mock_with :rspec
 
     # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
     config.fixture_path = "#{::Rails.root}/spec/fixtures"
 
     # If you're not using ActiveRecord, or you'd prefer not to run each of your
     # examples within a transaction, remove the following line or assign false
     # instead of true.
     config.use_transactional_fixtures = true
   end
+end
+
+Spork.each_run do
+  # This code will be run each time you run your specs.
+  
+end
+
+# --- Instructions ---
+# - Sort through your spec_helper file. Place as much environment loading 
+#   code that you don't normally modify during development in the 
+#   Spork.prefork block.
+# - Place the rest under Spork.each_run block
+# - Any code that is left outside of the blocks will be ran during preforking
+#   and during each_run!
+# - These instructions should self-destruct in 10 seconds.  If they don't,
+#   feel free to delete them.
+#



Sporkを使ったテスト

別のコンソールを開いてSporkを起動します。

$ bundle exec spork
Using RSpec
Loading Spork.prefork block...
Spork is ready and listening on 8989!

テストを実行します。

$ bundle exec rspec --drb spec/models/working_record_spec.rb 
....

Finished in 0.04636 seconds
4 examples, 0 failures

Sporkを使った場合と使わなかった場合を比較してみました。

使った場合
$ time bundle exec rspec --drb spec/models/working_record_spec.rb 
....

Finished in 0.03497 seconds
4 examples, 0 failures

real	0m1.514s
user	0m1.018s
sys	0m0.220s
使わなかった場合
$ time bundle exec rspec spec/models/working_record_spec.rb 
....

Finished in 0.01397 seconds
4 examples, 0 failures

real	0m6.470s
user	0m5.173s
sys	0m0.980s



.rspecの編集

デフォルトで--drbオプションが指定されるように、.rspecに記述を追加します。

FILE: $RAILS_ROOT/.rspec

@@ -1 +1,2 @@
 --colour
+--drb



更新されたクラスのリロード

Sporkはあらかじめテスト対象となるコードをロードしておくことで、テストを高速に実行しています。そのため、デフォルトではアプリケーションのコードを更新してもSporkには反映されません。しかし、そのたびにSporkを再起動していてはテストのリズムが悪くなるため、コードが更新されたらクラスをリロードするよう修正します。

FILE: $RAILS_ROOT/spec/spec_helper.rb

@@ -33,6 +33,11 @@ Spork.prefork do
     # instead of true.
     config.use_transactional_fixtures = true
   end
+
+  if Spork.using_spork?
+    ActiveSupport::Dependencies.clear
+    ActiveRecord::Base.instantiate_observers
+  end
 end
 
 Spork.each_run do
FILE: $RAILS_ROOT/config/application.rb

@@ -50,5 +50,13 @@ module Myapp
 
     # Version of your assets, change this if you want to expire all your assets
     config.assets.version = '1.0'
+
+    # Part of a Spork hack. See http://bit.ly/arY19y
+    if Rails.env.test? && defined?(Spork) && Spork.using_spork?
+      initializer :after => :initialize_dependency_mechanism do
+        # Work around initializer in railties/lib/rails/application/bootstrap.rb
+        ActiveSupport::Dependencies.mechanism = :load
+      end
+    end
   end
 end



SporkとGuardの連携

次のコマンドを実行すると、Guardfileが作成されます。

$ bundle exec guard init spork
Writing new Guardfile to /Users/tetsuyai/dev/projects/myapp/Guardfile
spork guard added to Guardfile, feel free to edit it
FILE: $RAILS_ROOT/Guardfile

# A sample Guardfile
# More info at https://github.com/guard/guard#readme

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

次のコマンドを実行すると、Sporkが起動します。

$ bundle exec guard start
Guard is now watching at '/Users/tetsuyai/dev/projects/daylife'
Starting Spork for RSpec 
Using RSpec
Loading Spork.prefork block...
Spork is ready and listening on 8989!
Spork server for RSpec successfully started



guard-rspecの設定

次のコマンドを実行すると、Guardfileに以下のような記述が追加されます。

$ bundle exec guard init rspec
rspec guard added to Guardfile, feel free to edit it
FILE: $RAILS_ROOT/Guardfile

@@ -8,3 +8,22 @@ guard 'spork', :cucumber_env => { 'RAILS_ENV' => 'test' }, :rspec_env => { 'RAIL
   watch(%r{^config/initializers/.+\.rb$})
   watch('spec/spec_helper.rb')
 end
+
+guard 'rspec', :version => 2 do
+  watch(%r{^spec/.+_spec\.rb$})
+  watch(%r{^lib/(.+)\.rb$})     { |m| "spec/lib/#{m[1]}_spec.rb" }
+  watch('spec/spec_helper.rb')  { "spec" }
+
+  # Rails example
+  watch(%r{^spec/.+_spec\.rb$})
+  watch(%r{^app/(.+)\.rb$})                           { |m| "spec/#{m[1]}_spec.rb" }
+  watch(%r{^lib/(.+)\.rb$})                           { |m| "spec/lib/#{m[1]}_spec.rb" }
+  watch(%r{^app/controllers/(.+)_(controller)\.rb$})  { |m| ["spec/routing/#{m[1]}_routing_spec.r
+  watch(%r{^spec/support/(.+)\.rb$})                  { "spec" }
+  watch('spec/spec_helper.rb')                        { "spec" }
+  watch('config/routes.rb')                           { "spec/routing" }
+  watch('app/controllers/application_controller.rb')  { "spec/controllers" }
+  # Capybara request specs
+  watch(%r{^app/views/(.+)/.*\.(erb|haml)$})          { |m| "spec/requests/#{m[1]}_spec.rb" }
+end
+

次のコマンドを実行すると、最初にすべてのスペックがテストされ、自動テストが始まります。

$ bundle exec guard start
Guard is now watching at '/Users/tetsuyai/dev/projects/myapp'
Starting Spork for RSpec 
Using RSpec
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
FF.FFFF..FFFFFFF*.............F..

これ以降、ファイルを保存すると自動テストが実行されます。