RSpecとCapybaraでJavaScript/Ajaxをテストする
RSpecはRubyのためのビヘイビア駆動開発(BDD)フレームワークで、Capybaraはブラウザの動きをシミュレートするRubyライブラリで、どちらもRubyGemsパッケージとして配布されています。Railsアプリケーションのテストを書く場合の定番の組み合わせといっていいでしょう。
「手順通りやったけどうまく行かなかった!」という方は、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)
(2012/10/01)
記事に関するご質問は、 hermes@oiax.jp までメールでお送りください。
ウェブサイト構築の発注先を検討されているお客様は、ご相談フォームをご利用ください。
- はじめに
- Rails はエンタープライズの世界で主流になるか (2008/03/29)
- Rails 2.0 ベンチマーク (2008/03/30)
- テスト駆動開発とデバッグコード (2008/04/07)
- Git or Mercurial (2008/04/22)
- RESTful Ruby on Rails -- その美しさと難しさ (2008/04/23)
- RESTful Ruby on Rails -- 単数と複数 (2008/04/24)
- Rails のソースコードを読む (2008/05/02)
- sudo: no passwd entry for app! (Capistrano 2.3.0) (2008/05/12)
- Rails on GlassFish (2008/06/18)
- 「Ruby は型宣言がないけど、ちゃんとしたシステムに使えるのか」という質問にどう答えるか (2008/11/23)
- 「Ruby はスクリプト言語だけど、遅くないですか」という質問にどう答えるか (2008/11/24)
- どのプログラミング言語が将来的に有望か (2009/01/10)
- どのフレームワークが将来的に有望か (2009/01/11)
- フレームワーク対 CMS (2009/01/12)
- Rails 2.3 で spawner/reaper は DEPRECATED に (2009/03/25)
- db/seeds.rb (2009/06/08)
- LESSとSass (2009/08/03)
- Rails 2.3.4 と I18n (2009/08/15)
- RubyGems のアップデート(上書きインストール) (2010/02/03)
- クリエイティブ・コモンズの Rails 教材 (2010/03/08)
- Yet Another Ruby Reference (2010/03/12)
- Rails で MongoDB を使ってみた (2010/03/23)
- Rails で MongoDB を使ってみた(2) (2010/03/25)
- 祝 Ruby on Rails 3.0 beta2 リリース (2010/04/02)
- Rails 3.x 時代のテストフレームワーク (2010/04/24)
- Test::Unit と RSpec と Shoulda (2010/05/06)
- 続・Rails 3.x 時代のテストフレームワーク (2010/05/06)
- Windows + Rails + MySQL 5.1 (2010/07/20)
- Ruby on Rails 2.3.9 リリース (2010/09/05)
- Railtie と Engine と Plugin の関係 (2010/10/23)
- Rails 3.0 と Internet Explorer (2010/11/19)
- acts_as_list: gem か plugin か (2010/11/29)
- さようならNetBeans/こんにちはRedcar (2011/01/28)
- RubyGems 1.5 が出たけど (2011/02/04)
- はじめる!Rails3 第2巻の執筆を始めました (2011/03/20)
- NetBeans 7.0にRuby on Railsプラグインをインストールする手順 (2011/03/21)
- Resqueを利用したRailsでの非同期処理/バッチ処理 (2011/03/23)
- Resqueワーカーをデーモンとして動かす (2011/04/02)
- はじめる!Rails3 第2巻の執筆(経過報告) (2011/04/23)
- はじめる!Rails3 第2巻の執筆(経過報告 No.2) (2011/05/11)
- Rails 3.1: assign_attributesメソッド (2011/05/14)
- Rails 3.1: has_secure_passwordメソッド (2011/05/27)
- はじめる!Rails3 第2巻の執筆(経過報告 No.3) (2011/06/06)
- Ruby on Rails 3.2 を Windows にインストールする手順をかなり丁寧に説明してみました (2011/07/22)
- Ruby on Rails 3.2 を Mac OS X にインストールする手順をかなり丁寧に説明してみました (2011/08/07)
- はじめる!Rails3 第2巻の執筆(経過報告 No.4) (2011/08/15)
- WindowsマシンにUbuntuをインストールしてRails開発を始めるには (2011/08/29)
- Rails 3.xでISO-2022-JP(JISコード)の電子メールを送る: mail-iso-2022-jp (2011/12/05)
- はじめる!Rails3 第3巻について (2011/12/30)
- NetBeans 7.1にRuby on Railsプラグインをインストールする手順 (2012/01/26)
- はじめる!Rails3 第3巻について(経過報告) (2012/02/29)
- はじめる!Rails3 第3巻について(経過報告2) (2012/03/17)
- Ruby on Railsで複合キーを扱う(1) (2012/03/25)
- Ruby on Railsで複合キーを扱う(2) (2012/03/26)
- Ruby on Railsで複合キーを扱う(3) (2012/03/27)
- Ruby on Railsで複合キーを扱う(4) (2012/03/28)
- Ruby on Railsで複合キーを扱う(5) (2012/03/29)
- Ruby on Railsで複合キーを扱う(6) -- 最終回 (2012/03/31)
- Ruby on Railsで複合キーを扱う(7) -- 補遺 (2012/04/01)
- Ruby on Rails 3.2 を Ubuntu にインストールする手順をかなり丁寧に説明してみました (2012/04/22)
- Ruby on Rails 3.2 を Cygwin にインストールする手順をかなり丁寧に説明してみました (2012/05/05)
- Ruby/Railsを学習・開発する環境としてのCygwin (2012/05/06)
- UbuntuにNetBeans IDE 7.1をインストールする手順 (2012/06/08)
- UbuntuにNetBeans IDE 7.2をインストールする手順 (2012/08/22)
- NetBeans 7.2にRuby on Railsプラグインをインストールする手順 (2012/08/31)
- RSpecとCapybaraでJavaScript/Ajaxをテストする (2012/10/01)
- Passenger 向けに AppArmor を設定する (2013/01/06)
- Railsセキュリティアップデートへの対処法 (2013/01/30)
- NetBeans 7.3にRuby on Railsプラグインをインストールする手順 (2013/05/02)

