FactoryBotでテストのはじめにデータを用意する
Fixturesの管理のしづらさに耐えかねてFactoryBotへ移行しようとしている,とあるRailsプロジェクトがあるのだが,移行に際して懸念していることがテストの低速化だ. Fixturesならテストの前にレコードを作成しテストでそれを使い回すことになる一方で,FactoryBotの場合は下手するとexampleの数だけINSERT文が走りテストの低速化を招く. Fixturesのように,FactoryBotを使ってテストの最初にレコードを作成することができればそれを回避することができると考え,仕組みを考えてみた.
想定状況
例えばUser
のようなModelはどのRailsプロジェクトにもあると思う.多くのAssosiationが定義されており,Userのレコードが様々なテストで必要となってくる.
そのような状況において,各exampleでUserレコードをINSERTしていては前述の通りコストがかかる.
このようなよく使うデータに関してはあらかじめDBに用意しておきたい.
(ただしデータは1度しか作成されないため,テストのランダム性は失なわれる.その点はトレードオフになる)
設定
spec/support/initialize_data.rb
というファイルを用意する.内容は以下の通り.
RSpec.shared_context 'initialize data' do
let(:test_user) { User.find(RSpec.configuration.test_data[:user]) }
end
RSpec.configure do |config|
config.add_setting :test_data
config.test_data = {}
config.before :suite do
config.test_data[:user] = FactoryBot.create(:user).id
end
config.include_context 'initialize data'
config.after :suite do
User.destroy_all
end
end
解説
データの作成と削除
RSpecのCallbackを設定できるタイミングは:suite
,:all
,:each
の3つがある1が今回のような「テスト開始前に1度だけ実行する」場合はbefore(:suite)
を使用する.
その中でFactoryBotを使ってレコードを作成する.
ただしbefore :suite
はトランザクションの外で行なわれるため,ここで作成したレコードは削除されずにテスト後も残ってしまう.
テストは毎回クリーンな環境で行いたいため,after :suite
内で手動でデータを削除する.
データへのアクセス
example内からここで作成したデータへアクセスするにはひと工夫必要になる.
インスタンス変数はbefore :suite
内で定義できない2ため,他の場所でDBから再度取得する必要がある.
この例だとUser.first
のように取得してもよいが,複数のデータを作成する場合はid
を使うのが安全だ.
idの値を他のスコープへ伝達するためにCustom settings
という機能を使う.3
RSpec.configure do |config|
config.add_setting :test_data
config.test_data = {}
config.before :suite do
config.test_data[:user] = FactoryBot.create(:user).id
end
:
end
データの取得
作成したデータを使わない場合は取得したくないためlet
で取得・定義する.
テストで共通のlet
を定義するにはshared_context
内に書き,RSpec.configuration
でそれをinclude_context
することになる.
RSpec.shared_context 'initialize data' do
let(:test_user) { User.find(RSpec.configuration.test_data[:user]) }
end
RSpec.configure do |config|
:
config.include_context 'initialize data'
:
end
使ってみる
例として以下のようなspecを用意した.このコードだとINSERTが1000回実行される
require 'rails_helper'
RSpec.describe User, type: :model do
let(:user) { FactoryBot.create(:user) }
1000.times do
it 'behaves like something' do
expect {
user.update_attributes(name: 'New Name')
}.to change(user, :name)
end
end
end
$ time bin/rspec
.......(省略)
Finished in 4.99 seconds (files took 0.5945 seconds to load)
1000 examples, 0 failures
bin/rspec 0.58s user 0.43s system 14% cpu 6.751 total
$ grep -c INSERT log/test.log
1000
上の設定を使ってみる.作成したデータへはtest_user
でインスタンスへアクセスできる.
INSERTは1回しか実行されない.
$ time bin/rspec
.......(省略)
Finished in 4.29 seconds (files took 0.53682 seconds to load)
1000 examples, 0 failures
bin/rspec 0.56s user 0.43s system 16% cpu 5.936 total
$ grep -c INSERT log/test.log
1
まだ短い期間しか運用していないため将来これで問題が起こるかもしれないが,アイデアとして記録を残しておく.
以上.
ここで挙げたコードは↓にある
uyorum/play-ruby-on-rails at rspec/initialize-with-factorybot
-
before
andafter
hooks - Hooks - RSpec Core - RSpec - Relish ↩︎ -
WARNING: Setting instance variables are not supported in before(:suite).
before
andafter
hooks - Hooks - RSpec Core - RSpec - Relish ↩︎ -
custom settings - Configuration - RSpec Core - RSpec - Relish ↩︎
関連記事
- RSpecでFakerを使うならKernel.srandを設定しておけという話
- Ruby on RailsのAsset Pipelineとインクルードとプリコンパイルの動作
- ワンタイムパスワード生成アルゴリズムについて学ぶ1 - HOTP