RSpecとCapybaraでJavaScript/Ajaxをテストする
2012/10/01
RSpecはRubyのためのビヘイビア駆動開発(BDD)フレームワークで、Capybaraはブラウザの動きをシミュレートするRubyライブラリで、どちらもRubyGemsパッケージとして配布されています。Railsアプリケーションのテストを書く場合の定番の組み合わせといっていいでしょう。
最近(2013年8月)、RSpec/Capybara入門という新連載を始めました。この記事でRSpec/Capybaraに興味を持った方は、こちらもどうぞ。
「手順通りやったけどうまく行かなかった!」という方は、hermes@oiax.jp までメールでお問い合わせください。また、「微妙に説明通りではなかった」という経験をした方も同アドレスまで情報をお寄せいただけると助かります。
この文章の内容は、随時更新しています。最終更新日: 2012/11/02
たとえば、ユーザーがあるRailsアプリケーションのトップページにアクセスして、a#touchme
をクリックしたら、次のページのp#message
に「Hello!」というテキストが含まれることをテストしたい場合、次のようなSpecファイルを書きます。
# coding: utf-8 require 'spec_helper' describe "Topページ" do before { visit root_path } context "a#touchmeをクリックすると" do before { find("a#touchme").click } specify do within('p#message') do expect(page).to have_content("Hello!") end end end end
expect(page).to have_content("Hello!")
はpage.should have_content("Hello!")
と書くこともできます。後者が伝統的な書き方で、前者はRSpec 2.11で導入された新しい書き方です。RSpecにしばしばコミットしているmyronmarstonのブログ記事 RSpec's New Expectation Syntax (2012/06/15)によれば、今後は前者の書き方が推奨され、将来的には明示的に有効にしない限り、should
は使えなくなるそうです。
さて、このRailsアプリケーションがJavaScript/Ajaxを利用していて、「span#touchme
をクリックしたら、p#message
の中に動的に「Hello!」というテキストが現れる」という仕様であった場合は、どうテストすればよいでしょうか。単にa#touchme
をspan#touchme
に変えるだけではだめです。CapybaraはJavaScriptを理解しないので、テストは失敗します。
ここで登場するのがcapybara-webkit
というドライバです。WebKitはオープンソースのHTMLレンダリングエンジンで、Google ChromeやSafariがこれを使っています。このドライバを使えばJavaScriptのテストが可能になります。
capybara-webkit
の特徴の一つは "headless" であることです。ここでいう "head" は、「デュアルヘッド」という用語と同様にディスプレイを指します。つまりcapybara-webkit
はディスプレイを操作しない、ということです。有名なSeleiumは実際にブラウザを起動して、APIを通じて操作します。CapybaraはデフォルトでSeleniumのドライバを内包しているのですぐに使えますが、テストするたびにブラウザが現れて画面がどんどん切り替わります。初めのうちは面白いし、失敗の原因が分かりやすいけれども、現実の開発で使っていると次第に煩わしくなります。
capybara-webkit
をインストールするには、例によってGemfile
に
gem "capybara-webkit"
と書いて、bundle install
を実行します。ただし、システムにQtのライブラリがインストールされていないと動きません。Mac OS X (Mountain Lion/Lion)の場合は、brew install qt
でインストールします。Ubuntuの場合なら、libqt4-dev
パッケージをapt-get
で入れておく必要があります。
もう一つ準備作業が必要です。spec/spec_helper.rb
の末尾に次の1行を追加してください。
Capybara.javascript_driver = :webkit
CapybaraはデフォルトでWebKitを使いません。describe
やcontext
メソッドにjs: true
オプションを付けると、その範囲のエグザンプル(RSpec用語では「テストケース」をこう言います)だけWebKitによるテストを行います。書き換え後のSpecファイルは次のようになります。
# coding: utf-8 require 'spec_helper' describe "Topページ" do before { visit root_path } context "span#touchmeをクリックすると", js: true do before { find("span#touchme").click } specify do within('#message') do expect(page).to have_content("Hello!") end end end end
これでテストが通るようになります。
さて、単純なテストはこれでいいのですが、データベースが絡むと少々ややこしい問題に遭遇します。実は、WebKitを使用する場合、Capybaraは別スレッドでRailsアプリケーションを起動します。そのためRSpec側とRailsアプリケーション側は別々のデータベース接続を持つことになります。
通常、RSpecは各エグザンプルの終わりにデータベースの状態を素早く元に戻すため、エグザンプルをトランザクションの中で実行します。エグザンプルが終わったらロールバックするんですね。トランザクションの中で行われたデータベース操作の結果は、コミットされるまで他のデータベースクライアントには見えません。RSpecの中でデータベースにレコードを追加しても、それがRailsアプリケーションには分からないのです。
この問題への伝統的な対処法は、JavaScriptを使ったRailsアプリケーションをCapybaraでテストする際には、spec/spec_helper.rb
の
config.use_transactional_fixtures = true
をfalse
に変え、config.after(:each)
ブロックの中でデータベースを空にするというものです。そのための専用のRubyGemsパッケージdatabase_cleaner
も存在しています。確かにこれでテストは通るようになりますが、トランザクションのロールバックを利用するよりもかなり遅いのが難点でした。
しかし、昨年(2011年)の12月にRailsコミッタの一人José Valimがブログ記事 Three tips to improve the performance of your test suite の中で素敵なハックを紹介したことで状況が変わりました。
この記事を参考に私が試してみたところ、以下の手順を踏めば、config.use_transactional_fixtures = true
のままでJavaScriptのテストができます。
まず、spec/support
ディレクトリに、次のような内容を持つ新規ファイルshared_connection.rb
を作成します。
class ActiveRecord::Base mattr_accessor :shared_connection @@shared_connection = nil def self.connection @@shared_connection || retrieve_connection end end
そして、spec/spec_helper.rb
の末尾に次の1行を追加します。
ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection
database_cleaner
のREADMEによれば、このハックを使うと「非決定論的な(non-deterministic)失敗を引き起こすことが報告されている」そうです。具体的にどういう報告があったのか調べられませんでしたが、この点に留意の上、この手法を採用するかどうか判断してください。
[追記] StackOverflow に投稿された回答によれば、mysql2
を使用している場合に Mysql2::Error This connection is still waiting for a result
というエラーが出て止まることがあるそうです。その回答は、RubyGemsパッケージconnection_pool
をGemfile
に加え、self.connection
メソッドの中身を@@shared_connection || ConnectionPool::Wrapper.new(:size => 1) { retrieve_connection }
で置き換えれば問題は解決すると言っています。ただし、私はこの現象をまだ確認していません。(2012/10/01)
重要 [追記] 私が携わっているRailsアプリケーション開発でこのハックを使ってみたところ、RSpecが途中でハングアウトする現象に遭遇しました。データベースにはPostgreSQLを使っています。PostgreSQLのログには SAVEPOINT can only be used in transaction blocks
というエラーメッセージが現れ、あるいは何のエラーメッセージも出さずに、それ以上テストの実行が進まなくなります。問題は未解決です。試しに、データベースをMySQLに切り替えてみたところテストの実行は最後まで進んだので、PostgreSQL特有の問題かもしれません。(2012/10/06)
[追記] 私は上記のハックを2週間ほどMySQLベースのRails開発で使ってみましたが、大きな問題は起きませんでした。ただし、Ajaxを使ってページを書き換えた直後に、RSpec側でデータベースアクセスを行うと Mysql2::Error This connection is still waiting for a result
というエラーが出ることがあります。Ajaxコールが完了するまで待ってからデータベースアクセスをするように書き換えれば問題は解消します。具体的には、Ajaxコールの直後に wait_until { page.evaluate_script('$.active') == 0 }
という行を追加してください。Mike Gehardのブログ記事を参考にしました。(2012/10/24)
このハックはSporkにも対応しています。Sporkはテスト対象のアプリケーションを読み込んでおいて、テストケース(エグザンプル)ごとにfork
してくれます。その結果、テストがより堅牢になり、たいていは時間も短縮されます。
Sporkの使い方は本稿のテーマではありませんが、簡単に説明します。例によってGemfile
に
gem "spork"
と書いて、bundle install
を実行します。続いて、spork rspec --bootstrap
を実行すると、spec/spec_helper.rb
が書き換えられます。
require 'rubygems' require 'spork' #uncomment the following line to use spork with the debugger #require 'spork/ext/ruby-debug' 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 --- (以下省略)
簡単に言えば、SporkはSpork.prefork
ブロックに書いてあるコードを1回だけ実行し、Spork.each_run
ブロックに書いてあるコードをエグザンプルを実行する直前に毎回実行します。
基本的には、# --- Instructions ---
よりも下にあるコード(もともとのspec_helper.rb
の中身)を全てSpork.prefork
ブロックに移動すればOKです。
ただし、José Valimのハックで追加した
ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection
は、Spork.each_run
ブロックに移す必要があります。
rspec
を実行するのとは別のターミナルを開いて、そこでspork
コマンドを実行するとテストサーバが起動します。そして--drb
オプションを付けてrspec
コマンドを実行すると、Spork経由でテストが実行されます。
常にSporkを使うなら.rspec
ファイルに--drb
を追加しておくといいでしょう。
Sporkによる時間短縮の効果はけっこう大きいです。ちょっとしたサンプルを作って試してみたところ、全体で約6秒かかっていたテストが約3秒になりました。
[更新] Mac OS XでQtをインストール手順を追加しました。(2012/10/01)
[更新] mysql2
で発生する問題についてのStackOverflowからの引用を追加しました。(2012/10/01)
[更新] PostgreSQLで発生する問題についての囲み記事を追加しました。(2012/10/06)
[更新] Ajaxコールとの関連で発生する問題についての囲み記事を追加しました。(2012/10/24)
[更新] --boostrap
を --bootstrap
に訂正。(2012/11/02)