JavaScriptを実行するフィーチャスペックでActiveRecordへのモンキーパッチを無くす方法


Railsのフィーチャスペックでよく起きる問題

Railsのフィーチャスペックでjs: trueにする(テスト実行時にJavaScriptも動かす)場合、フィーチャスペック内で作成したテストデータがブラウザ(PoltergeistやSelenium webdriverで起動するFirefox)内で参照できない、といった問題がよく発生する。

以下は問題が発生するフィーチャスペックの例である。

require 'rails_helper'

feature 'User management' do
  scenario "adds a new user", js: true do
    admin = create(:admin)

    visit root_path
    click_link 'Log In'
    fill_in 'Email', with: admin.email
    fill_in 'Password', with: admin.password
    # 上で作成したadminのデータがブラウザ側で参照できないのでログインに失敗する
    click_button 'Log In'

    # ...
  end
end

この問題はテストを実行しているコードのDBコネクションと、フィーチャスペック用のWebサーバーが使うDBコネクションが別々であるため、一方のDBトランザクション内で作成されたデータは他方のDBコネクションから参照できないことに起因している。

従来の古い解決策(モンキーパッチあり)

この問題を解決するために、特殊なモンキーパッチとDatabaseCleanerを使って、以下のような解決策がよく取られていた。

spec/support/shared_db_connection.rb
class ActiveRecord::Base
  mattr_accessor :shared_connection
  @@shared_connection = nil

  def self.connection
    @@shared_connection || retrieve_connection
  end
end
ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection
spec/rails_helper.rb
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }

RSpec.configure do |config|
  # ...

  config.use_transactional_fixtures = true

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

  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end

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

これは何をやっているかというと、テスト実行コードとフィーチャスペック用のWebサーバーでDBコネクションを共有して、どちらからでも同じデータの読み書きをできるようにしている。

最近の解決策(モンキーパッチなし)

しかし、ActiveRecordにモンキーパッチを当てるというのは黒魔術的であまり気持ちがいいものではない。

そこで、最新のDatabaseCleanerのREADMEでは、上記のモンキーパッチを使わずにこの問題を解決する方法が載っている。

spec/support/shared_db_connection.rb
# 不要になるのでファイルごと削除
spec/rails_helper.rb
RSpec.configure do |config|
  config.use_transactional_fixtures = false

  config.before(:suite) do
    if config.use_transactional_fixtures?
      raise(<<-MSG)
        設定がおかしいので警告メッセージを表示 (省略)
      MSG
    end
    DatabaseCleaner.clean_with(:truncation)
  end  

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

  config.before(:each, type: :feature) do
    driver_shares_db_connection_with_specs = Capybara.current_driver == :rack_test

    if !driver_shares_db_connection_with_specs
      DatabaseCleaner.strategy = :truncation
    end
  end

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

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

上記の設定は簡単にいうと次のようになっている。

  • js: true の場合( driver_shares_db_connection_with_specsfalse の場合)はトランザクションを使わずにデータを読み書きする。トランザクションを使わないので、どのDB接続からでも同じデータを参照できる。テストが終わったら全データをtruncateする。
  • それ以外の場合はDB接続は1つしかないので、トランザクション内でデータを読み書きする。

動作確認した実行環境

筆者は以下の環境で「モンキーパッチを使わない解決策」が有効に機能することを確認した。

古いバージョンのgemでは確認していないので、この解決策を適用する場合はテスト関連のgemはなるべく最新にすることが望ましい。

  • Rails 5.0.0
  • rspec-rails 3.5.0
  • capybara 2.7.1
  • database_cleaner 1.5.3
  • selenium-webdriver 2.53.4
  • factory_girl_rails 4.7.0

サンプルコード

Everyday Rails - RSpecによるRailsテスト入門のサンプルアプリケーションをベースに上記の解決策を適用してみた。

テストコードの内容は以下のGitHubリポジトリで確認できる。

この設定が解決するかもしれないトラブル

Rails 5にアップデートすると、リクエストスペックで次のようなエラーが発生してテストが落ちる場合がある。

PG::ConnectionBad: PQsocket() can't get socket descriptor: ROLLBACK TO SAVEPOINT active_record_1

テスト関連のgemを最新にし、モンキーパッチを使わない解決策を適用したところ、この問題が解消した。

その他:Everyday Railsの読者のみなさんへ

Everyday Rails - RSpecによるRailsテスト入門では「従来の古い解決策」が載っているので、適宜「モンキーパッチを使わない解決策」を使ってやってください。

具体的なスケジュールは未定ですが、Everyday RailsもRails 5に対応する予定なので、そのときには最新の解決策が載っているはずです

あわせて読みたい

Everyday RailsのサンプルアプリケーションをRails 5.0にアップグレードする方法を解説した記事です。
こちらもあわせてどうぞ。

これでもう怖くない!?Rails 4.1からRails 5.0にアップグレードする手順を動画付きで解説します - Qiita