#257 Request Specs and Capybara
- Download:
- source codeProject Files in Zip (131 KB)
- mp4Full Size H.264 Video (22.3 MB)
- m4vSmaller H.264 Video (14.8 MB)
- webmFull Size VP8 Video (39.1 MB)
- ogvFull Size Theora Video (31.3 MB)
ハイレベルテストは、Railsアプリケーションをテストする優れた方法です。この種のテストでよく用いられるCucumberについて、エピソード155[動画を見る, 読む]で取り上げました。Cucumberの、アプリケーションの振る舞いを英語の文法で定義するやり方が好きではない人もいるでしょう。そこで今回はハイレベルテストを実行する別の方法を見てみます。
普段はテスト駆動開発を奨励していますが、今回はすでに完成したアプリケーションにテストを追加することにします。それによって、実装の詳細やアプリケーションが正常動作するかどうかに気をとられることがなくなります。これからテストを追加するアプリケーションは、単純な1ページのみのTo Doリストです。タスクのリストを表示し、新規タスクを追加するためのフォームがあります。
このアプリケーションをテストするために、RSpec 2.0のrequest specを使用します。まず最初に、Gemfile
のdevelopementとtestグループにRSpec Rails gemを追加し、bundle
コマンドを実行します。
source 'http://rubygems.org' gem 'rails', '3.0.5' gem 'sqlite3' gem 'nifty-generators' group :development, :test do gem 'rspec-rails' end
Bundlerを実行後、以下のコマンドを実行してアプリケーションにRSpecを設定します。
$ rails g rspec:install
request specは、Railsに標準でついている統合テスト(エピソード187[動画を見る, 読む])に相当する機能のRSpec版です。request specを作成するためには、統合テストジェネレータを呼び出します。
$ rails g integration_test task
これによって、spec/requests
ディレクトリに次のようなtasks_spec.rb
ファイルが生成されます。
require 'spec_helper' describe "Tasks" do describe "GET /tasks" do it "works!(now write some real specs)" do # Run the generator again with the --webrat flag if you want to use webrat methods/matchers get tasks_path response.status.should be(200) end end end
このファイルの内容は標準的なRSpecテストに見えますが、違いとしては、get
のようなメソッドを使ってアプリケーションのページを呼び出せたり、response
を呼び出してリクエストに対するレスポンスを確認することができます。このファイルに標準で含まれるspecでは、上で示したタスクページを要求し、応答状態が200
、すなわち応答が成功であることをチェックします。このspecはそのまま修正なしで成功するはずなので、試してみましょう。rake spec:requests
を実行して、request specだけを実行します。
$ rake spec:requests (in /Users/eifion/code/tasklist) /Users/eifion/.rvm/rubies/ruby-1.9.2-p0/bin/ruby -S bundle exec rspec ./spec/requests/tasks_spec.rb DEPRECATION WARNING: <% %> style block helpers are deprecated. Please use <%= %>. (called from _app_views_tasks_index_html_erb___875755388255758006_2152410020_3563250333774836596 at /Users/eifion/code/tasklist/app/views/tasks/index.html.erb:3) <span class="passed">.</span> Finished in 0.18535 seconds <span class="passed">1 example, 0 failures</span>
specは成功しますが、非推奨の警告が表示されています。これがまさに、このようなテストを行うことで見つけることができるもので、直接ブラウザでテストしていたら見落とされてしまいます。どうやらブロックヘルパーでイコール記号(=)をつけ忘れたようです。request specを書くことで、さっそく有用なフィードバックを得ることができました。
フォームのビューファイルを見ると、最初のform_for
タグで、Rails 3では必要とされるイコール記号が抜けているのを確認できます。
<% form_for Task.new do |f| %>
これを以下のように修正します。
<%= form_for Task.new do |f| %>
ここで再度specを実行すると、specは成功し警告は出ません。
$ rake spec:requests (in /Users/eifion/code/tasklist) /Users/eifion/.rvm/rubies/ruby-1.9.2-p0/bin/ruby -S bundle exec rspec ./spec/requests/tasks_spec.rb <span class="passed">.</span> Finished in 0.16725 seconds <span class="passed">1 example, 0 failures</span>
最初の本格的なRequest Spec
通常はrequest specで単に応答状態のテストだけをしたいわけではありません。デフォルトのspecを書き換えて、ページにタスクが正しく表示されることをテストしてみます。
require 'spec_helper' describe "Tasks" do describe "GET /tasks" do it "displays tasks" do Task.create!(:name => "paint fence") get tasks_path response.body.should include("paint fence") end end end
このspecはわかりやすいでしょう。新しいタスクを作成して、tasks
ページにアクセスして、タスク名が表示されていることを確認します。rake spec:requests
を実行すると、ページにその文字列が含まれているので、specは成功します。
request specはRailsの統合テストの上で動作するので、統合テストが持つすべてのメソッドをサポートしています。例えば新規タスクの作成をテストしたい場合、post_via_redirect
メソッドを利用して、タスクが作成された後に正しくリダイレクトされたかを確認できます。
ではそのspecを書いてみましょう。まずpost_via_redirect
を呼び出して、index
ページにPOSTで引数を渡して“mow lawn(芝を刈る)”という新規タスクを作成します。その後で、結果として表示されるページにその文字列が存在するかどうかをチェックします。
require 'spec_helper' describe "Tasks" do # Other task omitted. describe "POST /tasks" do it "creates a task" do post_via_redirect tasks_path, ↵ :task => { :name => "mow lawn" } response.body.should include("mow lawn") end end end
ここでspecを実行すると、2つが成功しているので、コードが期待通りに動いたことがわかります。テスト駆動開発をおこなっている場合、失敗したspecに対して成功するようにコードを直すというように作業を進めます。これがテスト駆動開発の利点であり、これによりテストが正しく機能していることが保証されます。今回のアプリケーションでは、すでに稼働しているシステムなのでテストはいきなり成功してしまいました。すでにあるコードに対してテストを追加する場合、意図的にコードの一部を壊してテストが失敗することを確認し、その後また元に戻すという手順をふむのが重要です。
Capybaraでユーザインターフェイスをテストする
request specの問題の一つは、すべてのユーザ体験をテストするわけではないという点です。新規タスクの登録フォームを完全に壊したとしても、それをspecでとらえることはできません。これは、直接createアクションにPOSTしているからで、ユーザが実際にするようにフォームを介しているわけではないからです。
これを解決するためにはユーザの動作を真似る必要がありますが、Capybaraを使うことで実現できます。これはWebrat(エピソード156[動画を見る, 読む])の代替になるものです。Capybaraを利用すれば、ウェプアプリケーションでの利用者の振る舞いを真似ることが可能になります。Capybaraはgemなので、通常の方法でインストールします。同時にlaunchy gemも追加します。理由は後ほど説明します。Gemfile
でdevelopmentとtestの両方のグループにgemを追加し、 bundle
コマンドを実行してインストールを行います。
source 'http://rubygems.org' gem 'rails', '3.0.5' gem 'sqlite3' gem 'nifty-generators' group :development, :test do gem 'rspec-rails' gem 'capybara' gem 'launchy' end
Capybaraが自動的にrequest specに追加されるので、最初のspecでget
を呼び出す代わりにCapybaraのvisit
メソッドを利用します。またresponse.body.should include
をpage.should have_content
に置き換えます。
二つ目のspecでは、直接createアクションにPOSTする代わりに、Capybaraを利用してフォームへのデータ入力と登録をシミュレートします。fill_in
メソッドは “Name”のラベルがついたテキストボックスを探して値を設定し、click_button
メソッドが文字列からフォーム上のボタンを探しクリックします。
require 'spec_helper' describe "Tasks" do describe "GET /tasks" do it "displays tasks" do Task.create!(:name => "paint fence") visit tasks_path page.should have_content("paint fence") end end describe "POST /tasks" do it "creates a task" do visit tasks_path fill_in "Name", :with => "mow lawn" click_button "Add" page.should have_content("Successfully added task.") page.should have_content("mow lawn") end end end
これを実行するとspecは今回も成功します。そこでフォームもテストするために何か動作に問題が出る変更をしてみると、それが発見されます。
ページをデバッグする
想定通りspecが失敗したとして、その問題をどのようにデバッグすればいいのでしょうか? ここでlaunchyが登場します。Capybaraを使っている場合、どの時点でもsave_and_open_page
メソッドを呼び出すことによってそのページをブラウザで開いて確認することができます。これをclick_button "Add"
のすぐ後に追加し再度specを実行すると、内容をチェックする直前のタスクページを見ることができます。
ここで、その時点でのページの状態が表示されるとともに、フラッシュメッセージと、Capybaraによって追加されたタスクを見ることができます。
JavaScriptをテストする
アプリケーションのテストが十分に行われました。これらのテストは、request specとCapybaraを用いることによって簡単に追加することができました。しかし、もしアプリケーションにJavaScriptが含まれていてそれをテストしたい場合は、どうすればいいでしょうか? これは簡単に対応可能です。その方法を説明しましょう。
index
テンプレートにJavaScript を追加してテストできるようにします。単純にするため、コードは別ファイルにせず直接記述します。link_to_function
を使って、 JavaScript関数を呼び出すリンクを追加します。このリンクをクリックするとリンクの文字列が「js works」に変わります。実行されるスクリプトにはjQueryが必要なため、アプリケーションのレイアウトファイルで参照情報を追加しています。(ところで、Rails 3.1ではPrototypeに代わってjQueryがデフォルトのJavaScriptライブラリになります。)
<h1>Task List</h1> <%= link_to_function "test js", '$(this).html("js works")' %> <%= form_for Task.new do |f| %> <p> <%= f.label :name %> <%= f.text_field :name %> <%= f.submit "Add" %> </p> <% end %> <ul> <% @tasks.each do |task| %> <li><%= task.name %></li> <% end %> </ul>
ページに「test js」というリンクが表示され、クリックすると表示が変わります。
この新しく作成した機能を、Capybaraでテストしてみましょう。まず新しいspecを作成します。
it "supports js" do visit_tasks_path click_link "test js" page.should have_content("js works") end
request specを実行すると、1件の失敗が発生しています。
$ rake spec:requests (in /Users/eifion/code/tasklist) /Users/eifion/.rvm/rubies/ruby-1.9.2-p0/bin/ruby -S bundle exec rspec ./spec/requests/tasks_spec.rb .<span class="failed">F</span>. Failures: 1) Tasks GET /tasks supports js <span class="failed">Failure/Error: page.should have_content("js works")</span> <span class="failed">expected #has_content?("js works") to return true, got false</span> # ./spec/requests/tasks_spec.rb:14:in `block (3 levels) in <top (required)>' Finished in 0.83232 seconds <span class="failed">3 examples, 1 failure</span> rake aborted!ruby -S bundle exec rspec ./spec/requests/tasks_spec.rb failed (See full trace by running task with --trace)
specが失敗した原因は、そのページにJavaScriptで設定された文字列がなかったからです。デフォルトではCapybaraはJavaScriptをサポートしていません。そこでSeleniumを介してJavaScriptを使うよう指示します。
ここで示そうとしている機能は、現行バージョンのCapybaraではサポートされていません。Bundlerを利用して最新版をGitHubから入手します。
source 'http://rubygems.org' gem 'rails', '3.0.5' gem 'sqlite3' gem 'nifty-generators' group :development, :test do gem 'rspec-rails' gem 'capybara', :git => 'git://github.com/jnicklas/capybara.git' gem 'launchy' end
再度bundle
を実行すると、Capybara gemの最新版がダウンロードされてインストールされます。
さらに、JavaScriptのテストを可能にするために、 spec_helper
ファイルを修正してrequire 'rspec/rails'
という1行を追加します。
# 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' # rest of file...
この作業は正式リリース版では必要ないかも知れませんが、今のところは必要です。これで、Capybaraに対してspecでJavaScriptドライバを使用するように簡単に指定できるようになりました。specに:js => true
オプションを追加するだけです。
it "supports js", :js => true do visit tasks_path click_link "test js" page.should have_content("js works") end
specを実行すると、Firefoxが開いてJavaScriptを必要とするspecを実行します。これで再度すべてのspecが成功しました。
これは素晴らしい機能ですが、データベースのレコードに関連する問題がおそらく発生すると思われます。デモの目的で、最初の2つのspec(うちひとつは新しい Task
を作成するもの)に:js => true
オプションを追加します。
describe "Tasks" do describe "GET /tasks", :js => true do it "displays tasks" do Task.create!(:name => "paint fence") visit tasks_path page.should have_content("paint fence") end it "supports js" do visit tasks_path click_link "test js" page.should have_content("js works") end end end
ここでspecを実行すると、最初の2つのリクエストにSeleniumが使用されます。
$ rake spec:requests (in /Users/eifion/code/tasklist) /Users/eifion/.rvm/rubies/ruby-1.9.2-p0/bin/ruby -S bundle exec rspec ./spec/requests/tasks_spec.rb <span class="failed">F</span>.. Failures: 1) Tasks GET /tasks displays tasks <span class="failed">Failure/Error: page.should have_content("paint fence")</span> <span class="failed">expected there to be content "paint fence" in "Task List\ntest js\n\nName"</span> # ./spec/requests/tasks_spec.rb:8:in `block (3 levels) in <top (required)>' Finished in 7.69 seconds <span class="failed">3 examples, 1 failure</span>
今度は、最初のspecは「paint fence」がページにないということで失敗します。作成されるデータベースのレコードは、Seleniumテストからは見えません。これは、ここで試しているspecがデータベースのトランザクションを使用しているのですが、Seleniumとの互換性がないからです。これを修正するには、spec_helper
ファイル
のconfig.use_transactional_fixtures
の設定 をfalseにします。
# 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
これでテストは再度成功しますが、それはデータベースのレコードがspec間で流用されているということであり、これは望ましくありません。この問題を解決するために、database_cleanerというgemを利用して、specを実行するごとにデータベースを空にします。ドキュメントに、指定できるオプションについての説明があります。
このgemを使用するために、Gemfile
に参照情報を記述し、再度bundleコマンドを実行してインストールします。
source 'http://rubygems.org' gem 'rails', '3.0.5' gem 'sqlite3' gem 'nifty-generators' group :development, :test do gem 'rspec-rails' gem 'capybara', :git => 'git://github.com/jnicklas/capybara.git' gem 'launchy' gem 'database_cleaner' end
次にspec_helper
ファイルを再度修正して、spec毎にデータベースを空にするためのコードを追加します。
/spec/spec_helper.rb
config.before(:suite) do DatabaseCleaner.strategy = :truncation end config.before(:each) do DatabaseCleaner.start end config.after(:each) do DatabaseCleaner.clean end
specを実行すると、最初の2つは再度Seleniumを介して実行され、今回はデータベーストランザクションを使用していないので、テストは成功します。これは煩雑な作業に思われるかも知れませんが、一度設定ができてしまえば、ただ:js => true
オプションを追加するだけで、あらゆるspecについてJavaScriptを簡単にテストできるようになります。
Capybaraの次のリリースに含まれる、今回触れることができなかった機能として、specを定義するための新しいDSLがあります。新しくfeature
、background
、scenario
の各メソッドを利用して、Steak gemと同じ要領でspecを定義できるようになります。そのようにDSLを利用したい場合、Capybaraにその機能が組み込まれることによってSteakを使用せずにすみます。