RET

Reflection English Technology

FactoryBotで、特定の属性の値が異なる大量のテストデータを作りたい時のメモ

FactoryBotで大量のテストデータを作りたい時にはcreate_listメソッドが使える

create_list(:food, 100)

これでFoodのレコードが100件保存される。

では、消費期限が異なるFoodのレコードを100件作りたい場合はどうするか。

以下のようにcreate_listメソッドにはブロックが渡せる。

create_list(:food, 100) do |food, i|
  food.expiration_date = Date.today + i
end

これで、expiration_dateの値が異なるFoodのレコードを100件作れる。と思いきや、、、

expect(Food.all.pluck(:expiration_date)).to eq []

=>
expected: []
     got: [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil、、、(省略)

レコードは作られるものの、expiration_dateが全てnilになってしまう。

create_listにブロックを渡したときの挙動

下の記事の表現を引用させていただく。

https://dev.to/hernamvel/the-optimal-way-to-create-a-set-of-records-with-factorybot-createlist-factorybot-buildlist-1j64

create_listメソッドは以下のような挙動になる

def create_list
  object = build the object
  object.save!
  yield(object) if block_given?
end

つまり、foodをsaveした後に、そのsaveしたfoodをブロックに渡すという挙動になる。

そのため、すでにsaveされたfoodに対してexpiration_dateをセットしているものの、再度saveされていない(expiration_dateを更新していない)ためexpiration_dateはnilのままになる

解決策

以下のようにブロックの中でsave!をする。

create_list(:food, 100) do |food, i|
  food.expiration_date = Date.today + i
  food.save!
end

しかし、上のやり方だと、ひとつのオブジェクト(food)に対して2回saveが走る。

以下のようにbuild_listを使うと1オブジェクト1saveでテストデータを作成できる。

build_list(:food, 100) do |food, i|
  food.expiration_date = Date.today + i
  food.save!
end

ただ、このやり方で100件もsaveするとSQLの発行回数も100件になってしまい、テストが非常に重くなる。

ということで以下のようにバルクインサートする。

foods = build_list(:food, 100) do |food, i|
  food.expiration_date = Date.today + i
end

Food.insert_all foods.map(&:attributes)

参考文献