Capybara-webkitによる高速インテグレーションテストまとめ

このテのエントリも何度目になるかわかりませんが、
実際に使ってきて「これだ!」という環境がある程度確立できた気がするので
一旦ここにまとめておきます。Ruby1.9.3、Rails3.2.2で動作確認。

使うものリスト:

ライブラリ名 ちょっとした解説
Spork テストの高速化をしてくれるDRbサーバ
RSpec2 言わずと知れた「動く仕様書」
Guard ファイルを監視し、変更があればテストを自動実行*1
Capybara 独自のDSLでインテグレーションテストを可能にする
CapybaraWebkit CapybaraでJavaScriptの動作確認をするためのエンジン
FactoryGirl 「fixtureの代わり」とだけ表現するにはあまりにも惜しい多機能なモデルテンプレートエンジン
Headless ブラウザを画面上に表示せずにテストするためのラッパーライブラリ
DatabaseCleaner Capybaraへデータを渡すとき、渡したあとのデータ管理
Rails3Generators FactoryGirl用の定義を自動生成してくれるジェネレータ

autotestからGuardへ乗り換えたのは、Guardが常にプロンプトを表示しており、
rと叩くだけでSporkの再起動から各種specファイルの実行まで行なってくれる他、
MacならgrowlLinux系ならlibnotifyなど、通知までサポートしてくれる小粋な奴だからです。*2 *3

早速Gemfile:

group :development, :test do
  gem 'spork'
  gem 'rspec-rails'
  gem 'guard-spork'
  gem 'guard-rspec'
  gem 'capybara'
  gem 'capybara-webkit'
  gem 'factory_girl_rails'
  gem 'headless'
  gem 'database_cleaner'
  gem 'rails3-generators'
end

シェル:

$ sudo apt-get install xvfb         # headlessのための仮想ディスプレイライブラリ
$ sudo apt-get install libqt4-dev   # capybara-webkitのためのqt4ライブラリ
$ bundle install
$ bundle update
$ rails g rspec:install
$ spork --bootstrap

ここでSporkの設定が spec/spec_helper.rb の先頭に追加されているので、
Spork.prefork ブロックに元々の spec_helper.rb の内容を全て流し込みます。

そして、各gemの設定もついでにしてしまいましょう。

  • 2012-04-07追記: Sporkはテスト実行時にモデルをリロードしないようなので、
  • http://blog.twiwt.org/e/cafcfe
  • こちらを参考に Spork.each_run ブロックに追記しました。

spec/spec_helper.rb(一部コメントなどを消去しています):

Spork.prefork do
  # headless のための設定。
  # これをしないとテストのたびにブラウザが表示される(かもしれない)
  require "headless"
  headless = Headless.new(display:99)
  headless.start
  at_exit{ headless.destroy }

  # ここから下は元々の spec_helper.rb の内容
  ENV["RAILS_ENV"] ||= 'test'
  require File.expand_path("../../config/environment", __FILE__)
  require 'rspec/rails'
  require 'rspec/autorun'

  Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}

  RSpec.configure do |config|
    config.use_transactional_fixtures = true
    config.infer_base_class_for_anonymous_controllers = false

    # :focus => true のテストのみ実行する
    config.treat_symbols_as_metadata_keys_with_true_values = true
    config.filter_run :focus => true
    config.run_all_when_everything_filtered = true

    # database_cleanerの設定
    config.before :suite do
      DatabaseCleaner.strategy = :truncation
      DatabaseCleaner.clean_with :truncation
    end

    config.before :each do
      if example.metadata[:js]
        self.use_transactional_fixtures = false
        DatabaseCleaner.start
      end
    end

    config.after :each do
      if example.metadata[:js]
        DatabaseCleaner.clean
        self.use_transactional_fixtures = true
      end
    end
  end

  Capybara.javascript_driver = :webkit  # ドライバをcapybara-webkitに(デフォルトは :selenium)
end

Spork.each_run do
  FactoryGirl.reload   # ファクトリの定義が実行ごとにリロードされる
  silence_warnings do                                            # 2012-04-07追記
    Dir[Rails.root.join('app/**/*.rb')].each{ |file| load file } # 
  end                                                            #
end

続いてGuardの設定:

$ bundle exec guard init spork
$ bundle exec guard init rspec

このコマンドで Guardfile というものが作成されているはずです。
一見すると長くて記号だらけで読む気も起きませんが、
単に監視するファイルを正規表現で指定し、
対応するspecファイルを実行するようにしているだけです。

今回はFactoryGirlを使う他、
SporkとはDRbを使って通信してもらわなければならないので、
そこんところをちょっとだけ修正します。

# ↓コメントアウト
# guard 'rspec', :version => 2 do
guard 'rspec', :version => 2, :cli => '--drb' do  # rspecの引数に --drb を渡すことでSporkと通信してくれます
  watch(%r{^spec/factories/(.+)\.rb$})                { "spec/models" }  # 追加
  # ...
end

モデルを作成するとき、rails3-generatorsが spec/factories/ に
ファクトリファイルのひな形を置いてくれるようになっているので、
そのフォルダも監視してあげましょうという感じ。

そしてラスト、config/application.rb:

# Generators
module YourAppName
  class Application < Rails::Application
    # ...略...
    config.generators do |g|
      g.test_framework :rspec, fixture:true, views:false
      g.fixture_replacement :factory_girl
    end
  end
end

これでようやくスタートラインに立てました。
さっそくGuardを実行します。

$ bundle exec guard
Guard uses NotifySend to send notifications.
Guard is now watching at '/home/sandmark/sites/YourAppName'
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 ["--drb", "-f", "progress", "-r", "/home/sandmark/.rvm/gems/ruby-1.9.3-p125/gems/guard-rspec-0.7.0/lib/guard/rspec/formatters/notification_rspec.rb", "-f", "Guard::RSpec::Formatter::NotificationRSpec", "--out", "/dev/null", "--failure-exit-code", "2", "spec"]...
Run options: include {:focus=>true}

すでにモデルやコントローラを作成してあった場合は
大量の pending が出迎えてくれることでしょう。
ともあれ、何も問題がなければここでGuardのプロンプトが表示されているはずです。
(詳細については help と入力してEnterしといてください。)

さて、実際のspecファイルはこんな感じになります。
spec/requests/users_spec.rb:

describe "Users" do
  describe "GET /users" do
    before do
      @user = FactoryGirl.create(:user)
      visit "/users"
    end

    context "when 'about username' link was clicked," do
      it "should show user's name", :focus => true do
        click_link "about #{@user.name}"
        page.should have_content(@user.name)
      end

      it "should pop-up hidden message for JavaScript", :js => true do
        click_link "about #{@user.name}"
        page.should have_content("Woo Hoo!")
      end
    end  # context
  end  # GET /users
end  # Users

ここで it ディレクティブに渡しているのは :js と :focus ですが、
今回のエントリに直接関係があるのは :js オプションのほうです。

spec_helper.rb にも書きましたが、
:js => true を設定していると capybara-webkitJavaScriptエンジンとして動き、
Capybaraがブラウザ(今回のケースではwebkit-server)を起動、接続して動作をチェックするわけです。

しかし headless を導入しているのでブラウザのウィンドウは画面に表示されず、
メモリ上の仮想ディスプレイでうまいことテストしてくれる、というからくり。*4

ただ :webkit が呼び出されたときは webkit-server にデータを渡さなければならないので
RSpec2のタイミングでデータをリセットされると困ります。
そのため、 spec_helper.rb の DatabaseCleaner の設定部分で
一時的に RSpec2 でのデータ管理を止め、 DatabaseCleaner に任せています。

そして :focus => true のときは、Guardが変更を感知し、テストを開始しても、
:focus => true が設定されているテストのみを実行してくれるものです。
ひとつの機能を集中的に実装したい場合は特に役立つでしょう。

というわけで以上、もっと小さくまとめるつもりでしたが冗長な説明になってしまいました。
VMwareからネイティブUbuntuに変えたときに異常にテストが遅かった(50秒近くかかった)ので、
あるあ…ねーよwww と思って調査した次第です。

少しでもお役に立てれば。
それでは Happy Testing!

*1:SporkとRSpec2との仲介もしてくれる上に、結果を自動で通知してくれます。

*2:Windowsは知らん。

*3:Mac環境は持っていないのでわからないけど、growlって有料になったんだっけ…?

*4:capybara-webkitはそもそもX無しで動作するので headless 導入の意味があるかどうかは僕にはわかりません。