Hatena::ブログ(Diary)

=== SANDmark 19106 === beginning stress test このページをアンテナに追加 RSSフィード

2012年03月25日

RSpec2 + Capybara-Webkitでの注意点

昨日の記事 id:sandmark:20120324 に不備があったので補足です。
参考サイト: kinopyo blog - Learning through Writing

RSpec2はテストをそれぞれ実行するときデータベースをクリアしてくれますが、
capybara-webkit を使ったとき(it "hoge", :js => trueのとき)は
データが渡されない+クリアされない=テストが動かないので、何とかしてやる必要があります。

gem 'database_cleaner' を使ってRSpec2に設定をしてやります。

Gemfile:

group :test, :development do
  gem "database_cleaner"
end

spec/spec_helper.rb:(2012-04-03: 間違いです。後述。)

RSpec.configure do |config|
  # ...
  #
  # 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 = false   # true から false へ修正

  # 以下 DatabaseCleaner 用の設定を追記  
  config.before(:suite) do
    DatabaseCleaner.strategy = :truncation
  end

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

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

これによってテストのたびに DatabaseCleaner が実行され、
テスト用データベースをクリーンに保つことができます。

2012-04-03: 追記

テスト用データベースがクリーンに保たれるのは実に素晴らしいことなんですが、
ひとつひとつのテストのたびにデータを保存・クリアしているとやたら遅くなります。
90個弱あるテストが50秒弱で終わったので、ひとつのテストにつき2秒近くかかっている計算になります。

それはあんまりにもあんまりなので、DatabaseCleanerには必要なときだけ動いてもらいます。
というわけで上記 spec/spec_helper.rb の設定は間違い*1で、
現時点(database_cleaner0.7.2)では以下のコードが最適解のようです。

RSpec.configure do |config|
  # ...
  #
  # 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       # true のままにしておく

  # If true, the base class of anonymous controllers will be inferred
  # automatically. This will be the default behavior in future versions of
  # rspec-rails.
  config.infer_base_class_for_anonymous_controllers = false

  # database_cleaner
  config.before :suite do
    DatabaseCleaner.strategy = :truncation
    DatabaseCleaner.clean_with :truncation       # 追記
  end

  config.before :each do
    if example.metadata[:js]                     # example.metadata で it ディレクティブへの引数を参照できる
      # JavaScriptを使うテストではデータの受け渡しが不可欠なので、
      # 一時的にフィクスチャがクリアされないようにする。
      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

  Capybara.javascript_driver = :webkit
end

それでは改めて、みなさん快適なテスト駆動開発を!

scaffoldで生成したリソースのdestroy時のconfirmダイアログをテストする

参考: no title

scaffoldすると「Destroy」リンクをクリックしたときに
"Are you sure?" というJavaScriptのAlertボックスが表示されますが、
それをハンドリングしてテストしてみます。

def handle_js_confirm(accept=true)
  page.evaluate_script "window.original_confirm_function = window.confirm"
  page.evaluate_script "window.confirm = function(msg) { return #{!!accept}; }"
  yield
ensure
  page.evaluate_script "window.confirm = window.original_confirm_function"
end

まずはspecファイルにこんな感じのメソッドを定義します。
(どこに定義するのがベストなのかわかんないけど、spec_helper.rbの周辺が妥当ですかね…)

次に実際のインテグレーションテスト。

describe "Users" do
  it "shows yes/no dialog when destroy link was clicked", js:true do
    handle_js_confirm do
      click_link "削除"
    end
    page.should have_content("削除しました")
  end
end

日本語になってますが大体こんな雰囲気で。

*1:厳密には間違いではなく動作します。致命的に遅いだけ。

2012年03月24日

(今更)RSpec2+Capybara-webkitでインテグレーションテスト

今回参考にさせてもらったところ。
っていうかほとんどそのまんま。

あのさみんな、Cucumberに関する情報が少なすぎると思わないかい。
胸熱なのはわかったよ、でも使い方を教えてくれよ。俺頭悪いからわからないよ。*1
というわけでCucumberはさっくり諦めてRSpec2をこれまで以上に愛していきます。

組み合わせは Ruby1.9.3, Rails3.2, Spork, autotest, RSpec2, Capybara-webkit, headless の7つ。
前半5つについては過去の記事を参照してもらうことにして、
今回はCapybara-webkitとheadlessでJavaScriptのテストを有効にするところまで。
※今回は実際にJavaScriptのコードはテストしません。有効にするだけです。


Gemfile:

gem 'spork'
gem 'rspec-rails'
gem 'capybara-webkit'
gem 'autotest-rails'
gem 'headless'

JavaScriptを含めたインテグレーションテストは capybara-webkit で完結するんですが、
テストを実行するたびにFirefoxを起動するのでいろいろと心臓に悪いです。
autotest を使っている身としては尚更です。

そこで headless ですよ。
これは端的に言えばXのDISPLAYをメモリ上に展開する Xvfb というライブラリラッパーで、
capybara-webkit が起動するFirefoxには画面外で動作してもらおう、という力技。

まずはライブラリのほうをUbuntu流儀でインストールして、あとはいつものこと。

$ sudo apt-get install xvfb libqt4-dev
$ cd $RAILS_ROOT
$ bundle install
$ rails g rspec:install
$ spork --bootstrap

次に headless の設定。
黙っていてもcapybara-webkitがheadlessを使ってくれるように、
こちらは Spork 起動時に読み込んでもらうようにします。

spec/spec_helper.rb:

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.

  # ここから追加
  require "headless"
  headless = Headless.new(:display => 99)
  headless.start
  at_exit{ headless.destroy }
end

:display => 99 はネットで拾ってきた断片なのでよくわかりませんが、
とりあえずこれで動くのであまり触らないようにしておきます。

次はspecファイル。UsersControllerとかがあるとして、
spec/requests/users_spec.rb なんかにインテグレーションテストを記述します。
参考: no title

githubrspec-rails より一部転載・改変。

describe "Users" do
  it "works" do
    get "/login"
    response.status.should be(200)
  end

  it "displays username after successful login" do
    user = Factory(:user, :username => "jdoe", :password => "secret")  # FactoryGirlを使う場合
    visit "/login"
    fill_in "Username", :with => "jdoe"
    fill_in "Password", :with => "secret"
    click_button "ログイン"

    page.should have_selector(".header .username", :text => "jdoe")
    page.should have_content("ログインしました")
  end

  context "when JavaScript used" do
    it "displays help dialog without redirect", :js => true do
      visit "/login"
      click_link "ヘルプ"
      page.should have_content("これはヘルプです")
    end
  end
end
end

have_selectorはjQueryなどでお馴染みのセレクタを使って要素を選択し、
残りの引数で属性をチェックできます。
have_contentは「ページのどっかに文字列があること」みたいです。

JavaScriptを使う場合、itディレクティブに対して明示的に :js => true を渡す必要があります。
このオプションが渡されたテストのみcapybara-webkit(=Firefox)が起動し、
どういう仕組みなのかよくわかりませんがテストを実行してくれるわけです。

headlessを使っている以上Firefoxのウィンドウは画面に表示されませんが、
裏ではしっかり起動されているのでそれなりに時間がかかるのが難点ですかね。
我慢できなかったら大人しく他のJavaScript処理系を使ったほうが良さそうです。
まだろくに試していないので何とも言えません……。

さて、あとは .autotest 作って .rspec に --drb とか追記して、

$ spork
$ autotest

ね? 簡単でしょ?

参考1: RSpec2 + Capybara-Webkitでの注意点 - === SANDmark 19106 === beginning stress test
参考2: SporkとRSpec2とautotestとCucumber+Capybara(webkit)とtwitter-bootstrap-railsを使うときのセットアップが長い - === SANDmark 19106 === beginning stress test

*1:正確にはたくさん情報はあるんだけど、1年前のものが大半なので2012年現在と結構違ったりする。

2012年03月23日

namespaceとrspec - コントローラは名前空間に、モデルはトップに

管理者用に "/admin/" というプレフィクスをURLにつけたいことはよくあります。
Rails3では namespace ディレクティブでそれを実現させていますが、
実はこれ、コントローラのみならずモデルまで名前空間にあることを前提にしています。

例えばscaffoldするとわかるんですが、

$ rails g scaffold admin/user name:string password:string

なんてことをすると Admin::UsersController が app/controllers/admin/ に作成されます。
しかし同時に app/models/admin/user.rb に Admin::User クラスまで定義されてしまいます。
さらに、データベースのテーブル名にも名前空間を適用しているんです。

そりゃあ「そうしてください」って命令してるんで当然なんですけど、
コントローラ(とビュー)はいいんですよ。ただね、ただですよ、
Userモデルを他で使おうとすると、Adminモジュールの下にあるのは健康的じゃないだろうと。

namespaceじゃなくてscopeを使うとなると、これまたちょっと感覚的に違う。
そこで User はトップに持ってくるんだけど、そうすると今度は form_for が使えないねってお話。

長らく悩んでいたんですが、こちらの記事にズバリ書いてありました。
form_forに配列を渡すという発想は無かった。

その後「じゃあリダイレクトは?」とも思ったんですが、
こちらも redirect_to [:admin, @user] として解決しました。

response.should redirect_to([:admin, User.last])

こんな感じ。redirect_toはコントローラでも同じです。
探し方が悪いのか設計が悪いのか……あんまり情報無かったデス。