RSpec / Rails / Ruby

この記事はRuby on Rails Advent Calendar 2015の3日目です。

DatabaseCleanerでテスト実行毎にデータを削除する

RSpecでテストをする際に、各テストケースの実行毎にデータベースの状態をクリアしてくれるdatabase_cleanerというgemがあります。

このgemはとても便利で、rails_helper.rbに以下のような感じで設定しておくと他のテストの影響を受けずに各テストケースを実行できます。

RSpec.configure do |config|
  ...

  config.use_transactional_fixtures = false

  except_tables = %w(blog_categories)

  config.before(:suite) do
    # db/seeds.rbでblog_categoriesテーブルのデータを設定
    load Rails.root.join('db', 'seeds.rb')
    DatabaseCleaner.clean_with(:truncation, except: except_tables)
  end

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

  config.before(:example, js: true) do
    DatabaseCleaner.strategy = :truncation, {except: except_tables}
  end

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

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

  ## スマホ版のfeature specを書く場合などは以下の設定もしておく
  config.before(:example, type: :mobile) do
    Capybara.current_driver = :mobile_client
  end

  config.after(:example, type: :mobile) do
    Capybara.use_default_driver
  end
end

DatabaseCleanerのAUTOCOMMIT問題

ただ、transaction strategyで実行している場合はDatabaseCleaner.clean実行後もAUTOCOMMITされたデータが削除されません。

そのため「個別にテストを実行すると成功するのにrake specでは失敗する」といったことが起きてしまう場合があります。

AUTOCOMMIT問題の対策

で、そのための対策なのですが、rails_helper.rb

config.before(:example, truncation: true) do
  DatabaseCleaner.strategy = :truncation, {except: except_tables}
end

を追加して、AUTOCOMMITが行われるテストケースに

it 'performs AUTOCOMMIT', truncation: true do
  ...
end

truncation: trueを設定します。

こうするとこのテストケースはtruncation strategyで実行するのでAUTOCOMMITされたデータも削除してくれます。

AUTOCOMMITされるテストケースを見つける

ただ、どのテストケースでAUTOCOMMITが行われているのかをひとつひとつ確認するのは大変です。

なので、各テストケース終了後にデータが残っていないか確認するようにしておくと便利です。

例えば

config.after(:example) do
  DatabaseCleaner.clean
  [Blog::User, Blog::Tag].each do |model|
    raise "#{model} exists after example" if model.unscoped.exists?
  end
end

のようにしておくと、各テストケースの実行後にデータが残っている場合にちゃんとテストを失敗させることができます。

ちなみにunscopeddefault_scopeの影響を除外するために追加しています。

問題のあるテストケースが見つかれば、あとはtruncation: trueを設定するだけです。

チェックするモデルの選び方

全てのモデルのチェックをテスト実行毎に行うとテスト実行時間が長くなってしまうので、必要なモデルだけチェックするようにしましょう。

例えばBlog::SiteBlog::Userに依存している場合はBlog::Siteのチェックは不要です。

依存先がないモデルは全てチェックした方が良いのですが、全モデルのチェックは問題があった時に行うだけにして、毎回チェックするモデルは絞り込みましょう。

まとめ

テスト後にデータが残っているとAUTOCOMMITされたテストケースは成功するのに、その後で別のテストが失敗したりするので原因を探すのが結構大変です。

しかもテストの実行順によって時々失敗したり、失敗するテストケースも異なったりします。

この状態で放置しているとCIが機能しなくなってしまうので、AUTOCOMMITしたデータはしっかり削除するようにしましょう。

最後に最終的なrails_helper.rbの内容も載せておきます。

RSpec.configure do |config|
  ...

  config.use_transactional_fixtures = false

  except_tables = %w(blog_categories)

  config.before(:suite) do
    # db/seeds.rbでblog_categoriesテーブルのデータを設定
    load Rails.root.join('db', 'seeds.rb')
    DatabaseCleaner.clean_with(:truncation, except: except_tables)
  end

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

  config.before(:example, truncation: true) do
    DatabaseCleaner.strategy = :truncation, {except: except_tables}
  end

  config.before(:example, js: true) do
    DatabaseCleaner.strategy = :truncation, {except: except_tables}
  end

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

  config.after(:example) do
    DatabaseCleaner.clean
    [Blog::User, Blog::Tag].each do |model|
      raise "#{model} exists after example" if model.unscoped.exists?
    end
  end

  ## スマホ版のfeature specを書く場合などは以下の設定もしておく
  config.before(:example, type: :mobile) do
    Capybara.current_driver = :mobile_client
  end

  config.after(:example, type: :mobile) do
    Capybara.use_default_driver
  end
end