Tech Racho エンジニアの「?」を「!」に。
  • 開発

[翻訳+α] Rails/RSpec/Capybara/Seleniumでdatabase_cleaner gemを使う

こんにちは、hachi8833です。

Rails 4.1.1環境でなぜかPoltergeistでスクリーンショットを保存できない問題が解決したら記事にしようと思っていたのですが、間に合わなかったので今回はConfiguring database_cleaner with Rails, RSpec, Capybara and Seleniumという記事を翻訳してみました。
なお、この記事の内容は、弊社で行った「Crafting Rails 4 Applications読み会:第8回資料」と密接に関連します(スライド47 - 53ページあたり)。

解説

RSpecの1つ1つのテスト(beforeとafterも含めて1つの単位とします)は本来互いに独立すべきものなので、本来であればどのような順序で実行されても同じ結果にならなければなりません。だからこそRSpecは2.8からテスト実行順序をランダム化(config.order = "random")できるようになったのです。

しかしテストの量が増えてきたり古いRails環境だったりすると、データベースに残ったデータに知らず知らずに依存してしまうテストが増えてきます。その結果、「単体だと正常に実行できるのに全体テストでエラーになるテスト」や逆に「全体テストでは正常なのに単体だとエラーになるテスト」ができてしまいます。

前者は、たとえば同一のファクトリーを異なるテストでcreateしている場合が考えられます。それぞれのテストではファクトリーをcreateするのは正当なことですが、全体テストでは最初にcreateしたファクトリーが残留して、かつそのモデルで重複が禁止されていれば、2度目のcreateでエラーが生じてしまいます。

後者は、先に行われたテストで生成されたファクトリーに後のテストが依存している場合が考えられます。全体テストではとおりますが、単体だとファクトリーが足りずエラーになります。

database_cleanerを導入するのは、このような状態を避けるためでもあります。もっとも大量のテストがある巨大プロジェクトにdatabase_cleanerを追加すると、依存関係によるエラーが続々見つかって修正が大変かもしれませんが。

個人的には、テストはDRYを目指すより、前処理や後処理が多少重複しても独立性を目指すほうがよいように思います。

[翻訳]Rails/RSpec/Capybara/Seleniumでdatabase_cleaner gemを使う

元記事: Configuring database_cleaner with Rails, RSpec, Capybara and Selenium

Railsのコードを書いている人、Rubyでデータベースを扱うコードを書く人、テストを自動化している人なら、database_cleanerというgemを何となく知っていたり使ったりしたことがあることでしょう。これは実際よくできたgemで、さまざまなORM APIを抽象化してデータベースをきれいな白紙の状態に戻してくれます。

私は定期的にRSpec/Capybara/Selenumを使うRailsプロジェクトを新たに立ち上げていますが、そのたびにテストデータベースの一貫性が損なわれてしまうという憂き目に遭っています。せっかくテストデータベースにレコードをいくつか設定しても、Seleniumで駆動されるブラウザベースのテストは、そんなレコードなどどこにもないかのように振る舞います。最後にやっと問題点を解決し、そのたびに前にも同じ問題に遭遇して同じように解決していたことを思い出すのです。三歩歩くと忘れてしまうおのれの鳥頭が情けなくなります。

ぶつかる問題点は毎度同じです。テストはデータベーストランザクションによってラップされているので、実際のテストプロセスの外で実行されるあらゆるコード(つまり、Seleniumで駆動されるブラウザのリクエストに応答するサーバープロセスなど)からは、そのままでは私が作ったデータベースフィクスチャを参照できないのです。

ちょうどこの間ペアプログラミングを行ったときにも同じ問題を解決したので、そのときの作業を忘れないうちにメモします。皆様の髪の毛が抜けてしまわないために役立てば幸いです。

最初の手順、ここがとにかく肝心です。spec/spec_helper.rbを変更します。

config.use_transactional_fixtures = true

上の設定を以下のように変更してください。

config.use_transactional_fixtures = false

rspec-railsは暗黙のうちにデータベーストランザクションをラップしているのですが、これをオフにしておきます。そうしないと以下の設定が無意味になってしまいます。

次にdatabase_cleanerを設定します。私の場合、spec/support/database_cleaner.rbというファイルをそれ用に作成します。内容はだいたい以下のような感じです。

RSpec.configure do |config|

  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
  end

  config.before(:each) do
    DatabaseCleaner.strategy = :transaction
  end

  config.before(:each, :js => true) do
    DatabaseCleaner.strategy = :truncation
  end

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

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

end

解説

順に説明します。

config.before(:suite) do
  DatabaseCleaner.clean_with(:truncation)
end

上の設定は、テストスイート全体が実行される前にテストデータベースを完全に消去します。これにより、中断したテストや下手なテストコードによって生じたゴミを一掃し、テストが思わぬ振る舞いをするのを防ぎます。

config.before(:each) do
  DatabaseCleaner.strategy = :transaction
end

上の設定は、データベース消去のストラテジを"transaction"に設定します。トランザクションは非常に速いので、トランザクションを実行するすべてのテスト(実際のところ、RSpecプロセスで実行されるすべてのテスト)において、この設定を行っておくのがよいでしょう。

config.before(:each, :js => true) do
  DatabaseCleaner.strategy = :truncation
end

上の設定は、「:js => true」フラグが設定された例の前でのみ実行されます。デフォルトでは、Capybaraがテストサーバープロセスを起動してSeleniumバックエンド経由で実際のブラウザを駆動するのはこのようなテストだけです。このようなテストではトランザクションが動作しないので、このコードで設定を上書きしてストラテジを"truncation"に変更しています。

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

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

上の設定は、1つ1つのテストの実行前と実行後にdatabase_cleanerをフックしています。どのようなデータベース消去ストラテジを選択していても、database_cleanerが常に実行されるようにしています。

以上です。
上のコードはRSpec用であり、Cucumberの設定までは考慮していないことにご注意ください。

私が受けたストレスを皆様が避けることができれば幸いです。

追記

Gistで行っているようにすべてのスレッドを同一のActiveRecord接続で共有しないのはどうしてなのかという質問を何人かの方から受けました。いくつか理由を説明します。

  • database_cleaner を使用するのは、ORM(オブジェクト/関係マッピング)が中立であることを期待しているからです。database_cleaner はActiveRecord、DataMapper、MongoMapperなどをサポートしていますが、Gistで説明されているソリューションはActiveRecordでしか動作しません。
  • Gistのモンキーパッチによる方法は、ActiveRecordが内部での接続共有を変更しない間しか有効ではありません。正直、この方法がすべてのRuby VMとデータベースの組み合わせで動作するのかどうか私には確信がありません(追記: 現行のPostgreアダプタで競合が生じるという報告が2名からあがっています)。すべてのコンテキストで動作する設定オプションがActiveRecordにあれば私もこの方法をもっと使うかもしれません。
  • 上で強調したように、これを必要とするテストだけがtruncationにフォールバックするように注意を払いました。:js => trueを使用するテストの量は、私の場合多くはありません(そもそもブラウザを駆動するとどうしても速度が低下してしまいますから使い過ぎないようにしています)ので、私のソリューションで生じるオーバーヘッドについては気にしすぎないようにしています。受け入れテストでブラウザ駆動を全面的に行なうことがあればその点をもう少し気にしたでしょう。
  • データベースをtruncateするとテスト時間が大きく増加するのであれば、そのテストで特定のテーブルのサブセットだけをtruncateするように改良することで速度を向上できるでしょう。database_cleaner を採用しているのはそれがやりやすくなるからでもあります。これについては後日記事にするかもしれません。

追記: @donaldballから良い指摘がありました。テストとテストサーバー間のトランザクションを共有すると、production環境での実行と挙動が異なる部分が生じてしまいます。特に、production環境ではafter_commitフックは決して起動されません。

関連する投稿:

  1. Rack-Test and Capybara are uneasy bedfellows
  2. Announcing NullDB 0.0.1
  3. Complex Hash Expectations in RSpec

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。